diff --git a/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTest.java b/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTest.java
index e37000febf79b9d576f0619dc753b4d16aaa1b6f..d7c9c051dceeaf287d8c8e916892998f37448b90 100644
--- a/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTest.java
+++ b/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTest.java
@@ -66,6 +66,7 @@ import static org.briarproject.api.privategroup.Visibility.REVEALED_BY_CONTACT;
 import static org.briarproject.api.privategroup.Visibility.REVEALED_BY_US;
 import static org.briarproject.api.privategroup.Visibility.VISIBLE;
 import static org.briarproject.api.privategroup.invitation.GroupInvitationManager.CLIENT_ID;
+import static org.briarproject.api.sync.Group.Visibility.SHARED;
 import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
 import static org.briarproject.api.sync.ValidationManager.State.INVALID;
 import static org.briarproject.api.sync.ValidationManager.State.PENDING;
@@ -348,9 +349,9 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest {
 		assertEquals(joinMsg0.getMessage().getId(),
 				groupManager0.getPreviousMsgId(groupId0));
 
-		// make group visible to 1
+		// share the group with 1
 		Transaction txn0 = db0.startTransaction(false);
-		db0.setVisibleToContact(txn0, contactId1, privateGroup0.getId(), true);
+		db0.setGroupVisibility(txn0, contactId1, privateGroup0.getId(), SHARED);
 		db0.commitTransaction(txn0);
 		db0.endTransaction(txn0);
 
@@ -371,9 +372,10 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest {
 		assertEquals(joinMsg1.getMessage().getId(),
 				groupManager1.getPreviousMsgId(groupId0));
 
-		// make group visible to 0
+		// share the group with 0
 		Transaction txn1 = db1.startTransaction(false);
-		db1.setVisibleToContact(txn1, contactId01, privateGroup0.getId(), true);
+		db1.setGroupVisibility(txn1, contactId01, privateGroup0.getId(),
+				SHARED);
 		db1.commitTransaction(txn1);
 		db1.endTransaction(txn1);
 
@@ -415,9 +417,9 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest {
 		assertEquals(joinMsg0.getMessage().getId(),
 				groupManager0.getPreviousMsgId(groupId0));
 
-		// make group visible to 1
+		// share the group with 1
 		Transaction txn0 = db0.startTransaction(false);
-		db0.setVisibleToContact(txn0, contactId1, privateGroup0.getId(), true);
+		db0.setGroupVisibility(txn0, contactId1, privateGroup0.getId(), SHARED);
 		db0.commitTransaction(txn0);
 		db0.endTransaction(txn0);
 
@@ -438,9 +440,9 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest {
 		assertEquals(joinMsg1.getMessage().getId(),
 				groupManager1.getPreviousMsgId(groupId0));
 
-		// make group visible to 0
+		// share the group with 0
 		Transaction txn1 = db1.startTransaction(false);
-		db1.setVisibleToContact(txn1, contactId01, privateGroup0.getId(), true);
+		db1.setGroupVisibility(txn1, contactId01, privateGroup0.getId(), SHARED);
 		db1.commitTransaction(txn1);
 		db1.endTransaction(txn1);
 
@@ -518,9 +520,9 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest {
 	public void testRevealingRelationships() throws Exception {
 		defaultInit();
 
-		// make group visible to 2
+		// share the group with 2
 		Transaction txn0 = db0.startTransaction(false);
-		db0.setVisibleToContact(txn0, contactId2, privateGroup0.getId(), true);
+		db0.setGroupVisibility(txn0, contactId2, privateGroup0.getId(), SHARED);
 		db0.commitTransaction(txn0);
 		db0.endTransaction(txn0);
 
@@ -540,8 +542,9 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest {
 		Transaction txn2 = db2.startTransaction(false);
 		groupManager2.addPrivateGroup(txn2, privateGroup0, joinMsg2, false);
 
-		// make group visible to 0
-		db2.setVisibleToContact(txn2, contactId01, privateGroup0.getId(), true);
+		// share the group with 0
+		db2.setGroupVisibility(txn2, contactId01, privateGroup0.getId(),
+				SHARED);
 		db2.commitTransaction(txn2);
 		db2.endTransaction(txn2);
 
@@ -741,9 +744,9 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest {
 		assertEquals(joinMsg0.getMessage().getId(),
 				groupManager0.getPreviousMsgId(groupId0));
 
-		// make group visible to 1
+		// share the group with 1
 		Transaction txn0 = db0.startTransaction(false);
-		db0.setVisibleToContact(txn0, contactId1, privateGroup0.getId(), true);
+		db0.setGroupVisibility(txn0, contactId1, privateGroup0.getId(), SHARED);
 		db0.commitTransaction(txn0);
 		db0.endTransaction(txn0);
 
@@ -762,9 +765,10 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest {
 						inviteTime, creatorSignature);
 		groupManager1.addPrivateGroup(privateGroup0, joinMsg1, false);
 
-		// make group visible to 0
+		// share the group with 0
 		Transaction txn1 = db1.startTransaction(false);
-		db1.setVisibleToContact(txn1, contactId01, privateGroup0.getId(), true);
+		db1.setGroupVisibility(txn1, contactId01, privateGroup0.getId(),
+				SHARED);
 		db1.commitTransaction(txn1);
 		db1.endTransaction(txn1);
 		assertEquals(joinMsg1.getMessage().getId(),
diff --git a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
index b428e096bbdf2d562b2073e1504bd9505ec01df9..96ec6f17d38387157db5ee39c382912375485e5b 100644
--- a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
+++ b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
@@ -11,6 +11,7 @@ import org.briarproject.api.settings.Settings;
 import org.briarproject.api.sync.Ack;
 import org.briarproject.api.sync.ClientId;
 import org.briarproject.api.sync.Group;
+import org.briarproject.api.sync.Group.Visibility;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
@@ -232,6 +233,15 @@ public interface DatabaseComponent {
 	 */
 	Collection<Group> getGroups(Transaction txn, ClientId c) throws DbException;
 
+	/**
+	 * Returns the given group's visibility to the given contact, or
+	 * {@link Visibility INVISIBLE} if the group is not in the database.
+	 * <p/>
+	 * Read-only.
+	 */
+	Visibility getGroupVisibility(Transaction txn, ContactId c, GroupId g)
+			throws DbException;
+
 	/**
 	 * Returns the local pseudonym with the given ID.
 	 * <p/>
@@ -388,14 +398,6 @@ public interface DatabaseComponent {
 	void incrementStreamCounter(Transaction txn, ContactId c, TransportId t,
 			long rotationPeriod) throws DbException;
 
-	/**
-	 * Returns true if the given group is visible to the given contact.
-	 * <p/>
-	 * Read-only.
-	 */
-	boolean isVisibleToContact(Transaction txn, ContactId c, GroupId g)
-			throws DbException;
-
 	/**
 	 * Merges the given metadata with the existing metadata for the given
 	 * group.
@@ -470,6 +472,12 @@ public interface DatabaseComponent {
 	void setContactActive(Transaction txn, ContactId c, boolean active)
 			throws DbException;
 
+	/**
+	 * Sets the given group's visibility to the given contact.
+	 */
+	void setGroupVisibility(Transaction txn, ContactId c, GroupId g,
+			Visibility v) throws DbException;
+
 	/**
 	 * Marks the given message as shared.
 	 */
@@ -494,12 +502,6 @@ public interface DatabaseComponent {
 	void setReorderingWindow(Transaction txn, ContactId c, TransportId t,
 			long rotationPeriod, long base, byte[] bitmap) throws DbException;
 
-	/**
-	 * Makes a group visible or invisible to a contact.
-	 */
-	void setVisibleToContact(Transaction txn, ContactId c, GroupId g,
-			boolean visible) throws DbException;
-
 	/**
 	 * Stores the given transport keys, deleting any keys they have replaced.
 	 */
diff --git a/briar-api/src/org/briarproject/api/sync/Group.java b/briar-api/src/org/briarproject/api/sync/Group.java
index 0a1bc1ee3d350ea39492b27a57663e792ee58132..9ffd3b9cac2464316c4eb7a353e5c274a252dd1e 100644
--- a/briar-api/src/org/briarproject/api/sync/Group.java
+++ b/briar-api/src/org/briarproject/api/sync/Group.java
@@ -2,6 +2,12 @@ package org.briarproject.api.sync;
 
 public class Group {
 
+	public enum Visibility {
+		INVISIBLE, // The group is not visible
+		VISIBLE, // The group is visible but messages are not shared
+		SHARED // The group is visible and messages are shared
+	}
+
 	private final GroupId id;
 	private final ClientId clientId;
 	private final byte[] descriptor;
diff --git a/briar-core/src/org/briarproject/blogs/BlogManagerImpl.java b/briar-core/src/org/briarproject/blogs/BlogManagerImpl.java
index cfb4706894501bf75fdbd07f6791e0478660eb4a..b593dfce74807f1b5820aa627b97fb81029a31ae 100644
--- a/briar-core/src/org/briarproject/blogs/BlogManagerImpl.java
+++ b/briar-core/src/org/briarproject/blogs/BlogManagerImpl.java
@@ -12,7 +12,6 @@ import org.briarproject.api.blogs.MessageType;
 import org.briarproject.api.clients.Client;
 import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.contact.Contact;
-import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.contact.ContactManager;
 import org.briarproject.api.data.BdfDictionary;
 import org.briarproject.api.data.BdfEntry;
@@ -68,6 +67,7 @@ import static org.briarproject.api.blogs.MessageType.WRAPPED_COMMENT;
 import static org.briarproject.api.blogs.MessageType.WRAPPED_POST;
 import static org.briarproject.api.contact.ContactManager.AddContactHook;
 import static org.briarproject.api.contact.ContactManager.RemoveContactHook;
+import static org.briarproject.api.sync.Group.Visibility.SHARED;
 import static org.briarproject.blogs.BlogPostValidator.authorToBdfDictionary;
 
 @NotNullByDefault
@@ -96,35 +96,24 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager,
 
 	@Override
 	public void createLocalState(Transaction txn) throws DbException {
-		// Ensure that the local identity has its own personal blog
-		LocalAuthor la = identityManager.getLocalAuthor(txn);
-		Blog b = blogFactory.createBlog(la);
-		Group g = b.getGroup();
-		if (!db.containsGroup(txn, g.getId())) {
-			db.addGroup(txn, g);
-			for (ContactId c : db.getContacts(txn, la.getId())) {
-				db.setVisibleToContact(txn, c, g.getId(), true);
-			}
-		}
-		// Ensure that we have the personal blogs of all pre-existing contacts
+		// Create our personal blog if necessary
+		LocalAuthor a = identityManager.getLocalAuthor(txn);
+		Blog b = blogFactory.createBlog(a);
+		db.addGroup(txn, b.getGroup());
+		// Ensure that we have the personal blogs of all contacts
 		for (Contact c : db.getContacts(txn)) addingContact(txn, c);
 	}
 
 	@Override
 	public void addingContact(Transaction txn, Contact c) throws DbException {
-		// get personal blog of the contact
+		// Add the personal blog of the contact and share it with the contact
 		Blog b = blogFactory.createBlog(c.getAuthor());
-		Group g = b.getGroup();
-		if (!db.containsGroup(txn, g.getId())) {
-			// add the personal blog of the contact
-			db.addGroup(txn, g);
-			db.setVisibleToContact(txn, c.getId(), g.getId(), true);
-
-			// share our personal blog with the new contact
-			LocalAuthor a = db.getLocalAuthor(txn, c.getLocalAuthorId());
-			Blog b2 = blogFactory.createBlog(a);
-			db.setVisibleToContact(txn, c.getId(), b2.getId(), true);
-		}
+		db.addGroup(txn, b.getGroup());
+		db.setGroupVisibility(txn, c.getId(), b.getId(), SHARED);
+		// Share our personal blog with the contact
+		LocalAuthor a = identityManager.getLocalAuthor(txn);
+		Blog b2 = blogFactory.createBlog(a);
+		db.setGroupVisibility(txn, c.getId(), b2.getId(), SHARED);
 	}
 
 	@Override
diff --git a/briar-core/src/org/briarproject/db/Database.java b/briar-core/src/org/briarproject/db/Database.java
index fe16ac391c9ea39391d44d52f679a78ab103a1e2..1eb5c484d5045dcdbafb96808f52d4eb0bac9c81 100644
--- a/briar-core/src/org/briarproject/db/Database.java
+++ b/briar-core/src/org/briarproject/db/Database.java
@@ -12,6 +12,7 @@ import org.briarproject.api.identity.LocalAuthor;
 import org.briarproject.api.settings.Settings;
 import org.briarproject.api.sync.ClientId;
 import org.briarproject.api.sync.Group;
+import org.briarproject.api.sync.Group.Visibility;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
@@ -72,6 +73,13 @@ interface Database<T> {
 	 */
 	void addGroup(T txn, Group g) throws DbException;
 
+	/**
+	 * Sets the given group's visibility to the given contact to either
+	 * {@link Visibility VISIBLE} or {@link Visibility SHARED}.
+	 */
+	void addGroupVisibility(T txn, ContactId c, GroupId g, boolean shared)
+			throws DbException;
+
 	/**
 	 * Stores a local pseudonym.
 	 */
@@ -98,7 +106,7 @@ interface Database<T> {
 	 * Initialises the status of the given message with respect to the given
 	 * contact.
 	 *
-	 * @param ack  whether the message needs to be acknowledged.
+	 * @param ack whether the message needs to be acknowledged.
 	 * @param seen whether the contact has seen the message.
 	 */
 	void addStatus(T txn, ContactId c, MessageId m, boolean ack, boolean seen)
@@ -116,11 +124,6 @@ interface Database<T> {
 	void addTransportKeys(T txn, ContactId c, TransportKeys k)
 			throws DbException;
 
-	/**
-	 * Makes a group visible to the given contact.
-	 */
-	void addVisibility(T txn, ContactId c, GroupId g) throws DbException;
-
 	/**
 	 * Returns true if the database contains the given contact for the given
 	 * local pseudonym.
@@ -166,17 +169,9 @@ interface Database<T> {
 	boolean containsTransport(T txn, TransportId t) throws DbException;
 
 	/**
-	 * Returns true if the database contains the given group and the group is
-	 * visible to the given contact.
-	 * <p/>
-	 * Read-only.
-	 */
-	boolean containsVisibleGroup(T txn, ContactId c, GroupId g)
-			throws DbException;
-
-	/**
-	 * Returns true if the database contains the given message and the message
-	 * is visible to the given contact.
+	 * Returns true if the database contains the given message, the message is
+	 * shared, and the visibility of the message's group to the given contact
+	 * is either {@link Visibility VISIBLE} or {@link Visibility SHARED}.
 	 * <p/>
 	 * Read-only.
 	 */
@@ -267,6 +262,24 @@ interface Database<T> {
 	 */
 	Collection<Group> getGroups(T txn, ClientId c) throws DbException;
 
+	/**
+	 * Returns the given group's visibility to the given contact, or
+	 * {@link Visibility INVISIBLE} if the group is not in the database.
+	 * <p/>
+	 * Read-only.
+	 */
+	Visibility getGroupVisibility(T txn, ContactId c, GroupId g)
+			throws DbException;
+
+	/**
+	 * Returns the IDs of all contacts to which the given group's visibility is
+	 * either {@link Visibility VISIBLE} or {@link Visibility SHARED}.
+	 * <p/>
+	 * Read-only.
+	 */
+	Collection<ContactId> getGroupVisibility(T txn, GroupId g)
+			throws DbException;
+
 	/**
 	 * Returns the local pseudonym with the given ID.
 	 * <p/>
@@ -477,13 +490,6 @@ interface Database<T> {
 	Map<ContactId, TransportKeys> getTransportKeys(T txn, TransportId t)
 			throws DbException;
 
-	/**
-	 * Returns the IDs of all contacts to which the given group is visible.
-	 * <p/>
-	 * Read-only.
-	 */
-	Collection<ContactId> getVisibility(T txn, GroupId g) throws DbException;
-
 	/**
 	 * Increments the outgoing stream counter for the given contact and
 	 * transport in the given rotation period.
@@ -550,6 +556,13 @@ interface Database<T> {
 	 */
 	void removeGroup(T txn, GroupId g) throws DbException;
 
+	/**
+	 * Sets the given group's visibility to the given contact to
+	 * {@link Visibility INVISIBLE}.
+	 */
+	void removeGroupVisibility(T txn, ContactId c, GroupId g)
+			throws DbException;
+
 	/**
 	 * Removes a local pseudonym (and all associated state) from the database.
 	 */
@@ -585,11 +598,6 @@ interface Database<T> {
 	 */
 	void removeTransport(T txn, TransportId t) throws DbException;
 
-	/**
-	 * Makes a group invisible to the given contact.
-	 */
-	void removeVisibility(T txn, ContactId c, GroupId g) throws DbException;
-
 	/**
 	 * Resets the transmission count and expiry time of the given message with
 	 * respect to the given contact.
@@ -607,6 +615,13 @@ interface Database<T> {
 	void setContactActive(T txn, ContactId c, boolean active)
 			throws DbException;
 
+	/**
+	 * Sets the given group's visibility to the given contact to either
+	 * {@link Visibility VISIBLE} or {@link Visibility SHARED}.
+	 */
+	void setGroupVisibility(T txn, ContactId c, GroupId g, boolean shared)
+			throws DbException;
+
 	/**
 	 * Marks the given message as shared.
 	 */
diff --git a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
index 36ff8b29dbdaf0633ac51553ec3b7ef29f9385f8..392c0b90b18a086ee7c529ffcb858ed1318e04c6 100644
--- a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
+++ b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
@@ -42,6 +42,7 @@ import org.briarproject.api.settings.Settings;
 import org.briarproject.api.sync.Ack;
 import org.briarproject.api.sync.ClientId;
 import org.briarproject.api.sync.Group;
+import org.briarproject.api.sync.Group.Visibility;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
@@ -66,6 +67,8 @@ import java.util.logging.Logger;
 import javax.inject.Inject;
 
 import static java.util.logging.Level.WARNING;
+import static org.briarproject.api.sync.Group.Visibility.INVISIBLE;
+import static org.briarproject.api.sync.Group.Visibility.SHARED;
 import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
 import static org.briarproject.api.sync.ValidationManager.State.UNKNOWN;
 import static org.briarproject.db.DatabaseConstants.MAX_OFFERED_MESSAGES;
@@ -97,6 +100,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 	@Override
 	public boolean open() throws DbException {
 		Runnable shutdownHook = new Runnable() {
+			@Override
 			public void run() {
 				try {
 					close();
@@ -223,7 +227,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 	private void addMessage(T txn, Message m, State state, boolean shared,
 			@Nullable ContactId sender) throws DbException {
 		db.addMessage(txn, m, state, shared);
-		for (ContactId c : db.getVisibility(txn, m.getGroupId())) {
+		for (ContactId c : db.getGroupVisibility(txn, m.getGroupId())) {
 			boolean offered = db.removeOfferedMessage(txn, c, m.getId());
 			boolean seen = offered || c.equals(sender);
 			db.addStatus(txn, c, m.getId(), seen, seen);
@@ -441,6 +445,15 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		return db.getGroups(txn, c);
 	}
 
+	@Override
+	public Visibility getGroupVisibility(Transaction transaction, ContactId c,
+			GroupId g) throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
+		return db.getGroupVisibility(txn, c, g);
+	}
+
 	@Override
 	public LocalAuthor getLocalAuthor(Transaction transaction, AuthorId a)
 			throws DbException {
@@ -602,17 +615,6 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		db.incrementStreamCounter(txn, c, t, rotationPeriod);
 	}
 
-	@Override
-	public boolean isVisibleToContact(Transaction transaction, ContactId c,
-			GroupId g) throws DbException {
-		T txn = unbox(transaction);
-		if (!db.containsContact(txn, c))
-			throw new NoSuchContactException();
-		if (!db.containsGroup(txn, g))
-			throw new NoSuchGroupException();
-		return db.containsVisibleGroup(txn, c, g);
-	}
-
 	@Override
 	public void mergeGroupMetadata(Transaction transaction, GroupId g,
 			Metadata meta) throws DbException {
@@ -672,7 +674,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		T txn = unbox(transaction);
 		if (!db.containsContact(txn, c))
 			throw new NoSuchContactException();
-		if (db.containsVisibleGroup(txn, c, m.getGroupId())) {
+		if (db.getGroupVisibility(txn, c, m.getGroupId()) != INVISIBLE) {
 			if (db.containsMessage(txn, m.getId())) {
 				db.raiseSeenFlag(txn, c, m.getId());
 				db.raiseAckFlag(txn, c, m.getId());
@@ -745,7 +747,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		GroupId id = g.getId();
 		if (!db.containsGroup(txn, id))
 			throw new NoSuchGroupException();
-		Collection<ContactId> affected = db.getVisibility(txn, id);
+		Collection<ContactId> affected = db.getGroupVisibility(txn, id);
 		db.removeGroup(txn, id);
 		transaction.attach(new GroupRemovedEvent(g));
 		transaction.attach(new GroupVisibilityUpdatedEvent(affected));
@@ -794,6 +796,34 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		transaction.attach(new ContactStatusChangedEvent(c, active));
 	}
 
+	@Override
+	public void setGroupVisibility(Transaction transaction, ContactId c,
+			GroupId g, Visibility v) throws DbException {
+		if (transaction.isReadOnly()) throw new IllegalArgumentException();
+		T txn = unbox(transaction);
+		if (!db.containsContact(txn, c))
+			throw new NoSuchContactException();
+		if (!db.containsGroup(txn, g))
+			throw new NoSuchGroupException();
+		Visibility old = db.getGroupVisibility(txn, c, g);
+		if (old == v) return;
+		if (old == INVISIBLE) {
+			db.addGroupVisibility(txn, c, g, v == SHARED);
+			for (MessageId m : db.getMessageIds(txn, g)) {
+				boolean seen = db.removeOfferedMessage(txn, c, m);
+				db.addStatus(txn, c, m, seen, seen);
+			}
+		} else if (v == INVISIBLE) {
+			db.removeGroupVisibility(txn, c, g);
+			for (MessageId m : db.getMessageIds(txn, g))
+				db.removeStatus(txn, c, m);
+		} else {
+			db.setGroupVisibility(txn, c, g, v == SHARED);
+		}
+		List<ContactId> affected = Collections.singletonList(c);
+		transaction.attach(new GroupVisibilityUpdatedEvent(affected));
+	}
+
 	@Override
 	public void setMessageShared(Transaction transaction, MessageId m)
 			throws DbException {
@@ -845,33 +875,6 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		db.setReorderingWindow(txn, c, t, rotationPeriod, base, bitmap);
 	}
 
-	@Override
-	public void setVisibleToContact(Transaction transaction, ContactId c,
-			GroupId g, boolean visible) throws DbException {
-		if (transaction.isReadOnly()) throw new IllegalArgumentException();
-		T txn = unbox(transaction);
-		if (!db.containsContact(txn, c))
-			throw new NoSuchContactException();
-		if (!db.containsGroup(txn, g))
-			throw new NoSuchGroupException();
-		boolean wasVisible = db.containsVisibleGroup(txn, c, g);
-		if (visible && !wasVisible) {
-			db.addVisibility(txn, c, g);
-			for (MessageId m : db.getMessageIds(txn, g)) {
-				boolean seen = db.removeOfferedMessage(txn, c, m);
-				db.addStatus(txn, c, m, seen, seen);
-			}
-		} else if (!visible && wasVisible) {
-			db.removeVisibility(txn, c, g);
-			for (MessageId m : db.getMessageIds(txn, g))
-				db.removeStatus(txn, c, m);
-		}
-		if (visible != wasVisible) {
-			List<ContactId> affected = Collections.singletonList(c);
-			transaction.attach(new GroupVisibilityUpdatedEvent(affected));
-		}
-	}
-
 	@Override
 	public void updateTransportKeys(Transaction transaction,
 			Map<ContactId, TransportKeys> keys) throws DbException {
diff --git a/briar-core/src/org/briarproject/db/JdbcDatabase.java b/briar-core/src/org/briarproject/db/JdbcDatabase.java
index 3ee7fadd6fe9c336d5dd31c6663c3793b26c6e30..cce35cacad8b07d966f0246ef8cef5bd285337be 100644
--- a/briar-core/src/org/briarproject/db/JdbcDatabase.java
+++ b/briar-core/src/org/briarproject/db/JdbcDatabase.java
@@ -15,6 +15,7 @@ import org.briarproject.api.identity.LocalAuthor;
 import org.briarproject.api.settings.Settings;
 import org.briarproject.api.sync.ClientId;
 import org.briarproject.api.sync.Group;
+import org.briarproject.api.sync.Group.Visibility;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
@@ -50,6 +51,9 @@ import java.util.logging.Logger;
 
 import static java.util.logging.Level.WARNING;
 import static org.briarproject.api.db.Metadata.REMOVE;
+import static org.briarproject.api.sync.Group.Visibility.INVISIBLE;
+import static org.briarproject.api.sync.Group.Visibility.SHARED;
+import static org.briarproject.api.sync.Group.Visibility.VISIBLE;
 import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
 import static org.briarproject.api.sync.ValidationManager.State.INVALID;
 import static org.briarproject.api.sync.ValidationManager.State.PENDING;
@@ -67,8 +71,8 @@ import static org.briarproject.db.ExponentialBackoff.calculateExpiry;
  */
 abstract class JdbcDatabase implements Database<Connection> {
 
-	private static final int SCHEMA_VERSION = 28;
-	private static final int MIN_SCHEMA_VERSION = 28;
+	private static final int SCHEMA_VERSION = 29;
+	private static final int MIN_SCHEMA_VERSION = 29;
 
 	private static final String CREATE_SETTINGS =
 			"CREATE TABLE settings"
@@ -121,6 +125,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			"CREATE TABLE groupVisibilities"
 					+ " (contactId INT NOT NULL,"
 					+ " groupId HASH NOT NULL,"
+					+ " shared BOOLEAN NOT NULL,"
 					+ " PRIMARY KEY (contactId, groupId),"
 					+ " FOREIGN KEY (contactId)"
 					+ " REFERENCES contacts (contactId)"
@@ -356,6 +361,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		return s;
 	}
 
+	@Override
 	public Connection startTransaction() throws DbException {
 		Connection txn = null;
 		connectionsLock.lock();
@@ -384,6 +390,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		return txn;
 	}
 
+	@Override
 	public void abortTransaction(Connection txn) {
 		try {
 			txn.rollback();
@@ -414,6 +421,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public void commitTransaction(Connection txn) throws DbException {
 		try {
 			txn.commit();
@@ -429,7 +437,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	protected void closeAllConnections() throws SQLException {
+	void closeAllConnections() throws SQLException {
 		boolean interrupted = false;
 		connectionsLock.lock();
 		try {
@@ -455,11 +463,13 @@ abstract class JdbcDatabase implements Database<Connection> {
 		if (interrupted) Thread.currentThread().interrupt();
 	}
 
+	@Override
 	public DeviceId getDeviceId(Connection txn) throws DbException {
 		Settings s = getSettings(txn, DEVICE_SETTINGS_NAMESPACE);
 		return new DeviceId(StringUtils.fromHexString(s.get(DEVICE_ID_KEY)));
 	}
 
+	@Override
 	public ContactId addContact(Connection txn, Author remote, AuthorId local,
 			boolean verified, boolean active) throws DbException {
 		PreparedStatement ps = null;
@@ -497,6 +507,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public void addGroup(Connection txn, Group g) throws DbException {
 		PreparedStatement ps = null;
 		try {
@@ -515,6 +526,28 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
+	public void addGroupVisibility(Connection txn, ContactId c, GroupId g,
+			boolean shared) throws DbException {
+		PreparedStatement ps = null;
+		try {
+			String sql = "INSERT INTO groupVisibilities"
+					+ " (contactId, groupId, shared)"
+					+ " VALUES (?, ?, ?)";
+			ps = txn.prepareStatement(sql);
+			ps.setInt(1, c.getInt());
+			ps.setBytes(2, g.getBytes());
+			ps.setBoolean(3, shared);
+			int affected = ps.executeUpdate();
+			if (affected != 1) throw new DbStateException();
+			ps.close();
+		} catch (SQLException e) {
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
+	@Override
 	public void addLocalAuthor(Connection txn, LocalAuthor a)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -537,6 +570,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public void addMessage(Connection txn, Message m, State state,
 			boolean shared) throws DbException {
 		PreparedStatement ps = null;
@@ -562,6 +596,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public void addOfferedMessage(Connection txn, ContactId c, MessageId m)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -592,6 +627,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public void addStatus(Connection txn, ContactId c, MessageId m, boolean ack,
 			boolean seen) throws DbException {
 		PreparedStatement ps = null;
@@ -613,6 +649,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public void addMessageDependency(Connection txn, GroupId g,
 			MessageId dependent, MessageId dependency) throws DbException {
 		PreparedStatement ps = null;
@@ -633,6 +670,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public void addTransport(Connection txn, TransportId t, int maxLatency)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -651,6 +689,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public void addTransportKeys(Connection txn, ContactId c, TransportKeys k)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -712,24 +751,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public void addVisibility(Connection txn, ContactId c, GroupId g)
-			throws DbException {
-		PreparedStatement ps = null;
-		try {
-			String sql = "INSERT INTO groupVisibilities (contactId, groupId)"
-					+ " VALUES (?, ?)";
-			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			ps.setBytes(2, g.getBytes());
-			int affected = ps.executeUpdate();
-			if (affected != 1) throw new DbStateException();
-			ps.close();
-		} catch (SQLException e) {
-			tryToClose(ps);
-			throw new DbException(e);
-		}
-	}
-
+	@Override
 	public boolean containsContact(Connection txn, AuthorId remote,
 			AuthorId local) throws DbException {
 		PreparedStatement ps = null;
@@ -753,6 +775,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public boolean containsContact(Connection txn, ContactId c)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -774,6 +797,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public boolean containsGroup(Connection txn, GroupId g)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -795,6 +819,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public boolean containsLocalAuthor(Connection txn, AuthorId a)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -816,6 +841,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public boolean containsMessage(Connection txn, MessageId m)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -837,6 +863,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public boolean containsTransport(Connection txn, TransportId t)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -858,29 +885,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public boolean containsVisibleGroup(Connection txn, ContactId c, GroupId g)
-			throws DbException {
-		PreparedStatement ps = null;
-		ResultSet rs = null;
-		try {
-			String sql = "SELECT NULL FROM groupVisibilities"
-					+ " WHERE contactId = ? AND groupId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			ps.setBytes(2, g.getBytes());
-			rs = ps.executeQuery();
-			boolean found = rs.next();
-			if (rs.next()) throw new DbStateException();
-			rs.close();
-			ps.close();
-			return found;
-		} catch (SQLException e) {
-			tryToClose(rs);
-			tryToClose(ps);
-			throw new DbException(e);
-		}
-	}
-
+	@Override
 	public boolean containsVisibleMessage(Connection txn, ContactId c,
 			MessageId m) throws DbException {
 		PreparedStatement ps = null;
@@ -891,7 +896,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " ON m.groupId = gv.groupId"
 					+ " WHERE messageId = ?"
 					+ " AND contactId = ?"
-					+ " AND shared = TRUE";
+					+ " AND m.shared = TRUE";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, m.getBytes());
 			ps.setInt(2, c.getInt());
@@ -908,6 +913,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public int countOfferedMessages(Connection txn, ContactId c)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -931,6 +937,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public void deleteMessage(Connection txn, MessageId m) throws DbException {
 		PreparedStatement ps = null;
 		try {
@@ -947,6 +954,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public void deleteMessageMetadata(Connection txn, MessageId m)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -963,6 +971,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public Contact getContact(Connection txn, ContactId c) throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
@@ -992,6 +1001,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public Collection<Contact> getContacts(Connection txn)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -1017,7 +1027,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			}
 			rs.close();
 			ps.close();
-			return Collections.unmodifiableList(contacts);
+			return contacts;
 		} catch (SQLException e) {
 			tryToClose(rs);
 			tryToClose(ps);
@@ -1025,6 +1035,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public Collection<ContactId> getContacts(Connection txn, AuthorId local)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -1039,7 +1050,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			while (rs.next()) ids.add(new ContactId(rs.getInt(1)));
 			rs.close();
 			ps.close();
-			return Collections.unmodifiableList(ids);
+			return ids;
 		} catch (SQLException e) {
 			tryToClose(rs);
 			tryToClose(ps);
@@ -1047,6 +1058,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public Collection<Contact> getContactsByAuthorId(Connection txn,
 			AuthorId remote) throws DbException {
 		PreparedStatement ps = null;
@@ -1073,7 +1085,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			}
 			rs.close();
 			ps.close();
-			return Collections.unmodifiableList(contacts);
+			return contacts;
 		} catch (SQLException e) {
 			tryToClose(rs);
 			tryToClose(ps);
@@ -1081,6 +1093,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public Group getGroup(Connection txn, GroupId g) throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
@@ -1103,6 +1116,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public Collection<Group> getGroups(Connection txn, ClientId c)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -1121,7 +1135,33 @@ abstract class JdbcDatabase implements Database<Connection> {
 			}
 			rs.close();
 			ps.close();
-			return Collections.unmodifiableList(groups);
+			return groups;
+		} catch (SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
+	@Override
+	public Visibility getGroupVisibility(Connection txn, ContactId c, GroupId g)
+			throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			String sql = "SELECT shared FROM groupVisibilities"
+					+ " WHERE contactId = ? AND groupId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setInt(1, c.getInt());
+			ps.setBytes(2, g.getBytes());
+			rs = ps.executeQuery();
+			Visibility v;
+			if (rs.next()) v = rs.getBoolean(1) ? SHARED : VISIBLE;
+			else v = INVISIBLE;
+			if (rs.next()) throw new DbStateException();
+			rs.close();
+			ps.close();
+			return v;
 		} catch (SQLException e) {
 			tryToClose(rs);
 			tryToClose(ps);
@@ -1129,6 +1169,30 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
+	public Collection<ContactId> getGroupVisibility(Connection txn, GroupId g)
+			throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			String sql = "SELECT contactId FROM groupVisibilities"
+					+ " WHERE groupId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setBytes(1, g.getBytes());
+			rs = ps.executeQuery();
+			List<ContactId> visible = new ArrayList<ContactId>();
+			while (rs.next()) visible.add(new ContactId(rs.getInt(1)));
+			rs.close();
+			ps.close();
+			return visible;
+		} catch (SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
+	@Override
 	public LocalAuthor getLocalAuthor(Connection txn, AuthorId a)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -1158,6 +1222,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public Collection<LocalAuthor> getLocalAuthors(Connection txn)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -1179,7 +1244,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			}
 			rs.close();
 			ps.close();
-			return Collections.unmodifiableList(authors);
+			return authors;
 		} catch (SQLException e) {
 			tryToClose(rs);
 			tryToClose(ps);
@@ -1187,6 +1252,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public Collection<MessageId> getMessageIds(Connection txn, GroupId g)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -1200,7 +1266,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			while (rs.next()) ids.add(new MessageId(rs.getBytes(1)));
 			rs.close();
 			ps.close();
-			return Collections.unmodifiableList(ids);
+			return ids;
 		} catch (SQLException e) {
 			tryToClose(rs);
 			tryToClose(ps);
@@ -1223,7 +1289,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			while (rs.next()) ids.add(new MessageId(rs.getBytes(1)));
 			rs.close();
 			ps.close();
-			return Collections.unmodifiableList(ids);
+			return ids;
 		} catch (SQLException e) {
 			tryToClose(rs);
 			tryToClose(ps);
@@ -1231,6 +1297,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public Collection<MessageId> getMessageIds(Connection txn, GroupId g,
 			Metadata query) throws DbException {
 		// If there are no query terms, return all delivered messages
@@ -1262,8 +1329,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 				// Return early if there are no matches
 				if (intersection.isEmpty()) return Collections.emptySet();
 			}
-			if (intersection == null) throw new IllegalStateException();
-			return Collections.unmodifiableSet(intersection);
+			if (intersection == null) throw new AssertionError();
+			return intersection;
 		} catch (SQLException e) {
 			tryToClose(rs);
 			tryToClose(ps);
@@ -1271,6 +1338,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public Map<MessageId, Metadata> getMessageMetadata(Connection txn,
 			GroupId g) throws DbException {
 		PreparedStatement ps = null;
@@ -1300,7 +1368,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			}
 			rs.close();
 			ps.close();
-			return Collections.unmodifiableMap(all);
+			return all;
 		} catch (SQLException e) {
 			tryToClose(rs);
 			tryToClose(ps);
@@ -1308,6 +1376,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public Map<MessageId, Metadata> getMessageMetadata(Connection txn,
 			GroupId g, Metadata query) throws DbException {
 		// Retrieve the matching message IDs
@@ -1317,9 +1386,10 @@ abstract class JdbcDatabase implements Database<Connection> {
 		Map<MessageId, Metadata> all = new HashMap<MessageId, Metadata>(
 				matches.size());
 		for (MessageId m : matches) all.put(m, getMessageMetadata(txn, m));
-		return Collections.unmodifiableMap(all);
+		return all;
 	}
 
+	@Override
 	public Metadata getGroupMetadata(Connection txn, GroupId g)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -1342,6 +1412,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public Metadata getMessageMetadata(Connection txn, MessageId m)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -1367,6 +1438,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public Metadata getMessageMetadataForValidator(Connection txn, MessageId m)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -1394,6 +1466,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public Collection<MessageStatus> getMessageStatus(Connection txn,
 			ContactId c, GroupId g) throws DbException {
 		PreparedStatement ps = null;
@@ -1418,7 +1491,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			}
 			rs.close();
 			ps.close();
-			return Collections.unmodifiableList(statuses);
+			return statuses;
 		} catch (SQLException e) {
 			tryToClose(rs);
 			tryToClose(ps);
@@ -1426,6 +1499,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public MessageStatus getMessageStatus(Connection txn,
 			ContactId c, MessageId m) throws DbException {
 		PreparedStatement ps = null;
@@ -1453,6 +1527,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public Map<MessageId, State> getMessageDependencies(Connection txn,
 			MessageId m) throws DbException {
 		PreparedStatement ps = null;
@@ -1482,7 +1557,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			}
 			rs.close();
 			ps.close();
-			return Collections.unmodifiableMap(dependencies);
+			return dependencies;
 		} catch (SQLException e) {
 			tryToClose(rs);
 			tryToClose(ps);
@@ -1490,6 +1565,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public Map<MessageId, State> getMessageDependents(Connection txn,
 			MessageId m) throws DbException {
 		PreparedStatement ps = null;
@@ -1511,7 +1587,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			}
 			rs.close();
 			ps.close();
-			return Collections.unmodifiableMap(dependents);
+			return dependents;
 		} catch (SQLException e) {
 			tryToClose(rs);
 			tryToClose(ps);
@@ -1519,6 +1595,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public State getMessageState(Connection txn, MessageId m)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -1541,6 +1618,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public Collection<MessageId> getMessagesToAck(Connection txn, ContactId c,
 			int maxMessages) throws DbException {
 		PreparedStatement ps = null;
@@ -1557,7 +1635,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			while (rs.next()) ids.add(new MessageId(rs.getBytes(1)));
 			rs.close();
 			ps.close();
-			return Collections.unmodifiableList(ids);
+			return ids;
 		} catch (SQLException e) {
 			tryToClose(rs);
 			tryToClose(ps);
@@ -1565,6 +1643,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public Collection<MessageId> getMessagesToOffer(Connection txn,
 			ContactId c, int maxMessages) throws DbException {
 		long now = clock.currentTimeMillis();
@@ -1572,10 +1651,13 @@ abstract class JdbcDatabase implements Database<Connection> {
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT m.messageId FROM messages AS m"
+					+ " JOIN groupVisibilities AS gv"
+					+ " ON m.groupId = gv.groupId"
 					+ " JOIN statuses AS s"
 					+ " ON m.messageId = s.messageId"
-					+ " WHERE contactId = ?"
-					+ " AND state = ? AND shared = TRUE AND raw IS NOT NULL"
+					+ " AND gv.contactId = s.contactId"
+					+ " WHERE gv.contactId = ? AND gv.shared = TRUE"
+					+ " AND state = ? AND m.shared = TRUE AND raw IS NOT NULL"
 					+ " AND seen = FALSE AND requested = FALSE"
 					+ " AND expiry < ?"
 					+ " ORDER BY timestamp LIMIT ?";
@@ -1589,7 +1671,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			while (rs.next()) ids.add(new MessageId(rs.getBytes(1)));
 			rs.close();
 			ps.close();
-			return Collections.unmodifiableList(ids);
+			return ids;
 		} catch (SQLException e) {
 			tryToClose(rs);
 			tryToClose(ps);
@@ -1597,6 +1679,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public Collection<MessageId> getMessagesToRequest(Connection txn,
 			ContactId c, int maxMessages) throws DbException {
 		PreparedStatement ps = null;
@@ -1613,7 +1696,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			while (rs.next()) ids.add(new MessageId(rs.getBytes(1)));
 			rs.close();
 			ps.close();
-			return Collections.unmodifiableList(ids);
+			return ids;
 		} catch (SQLException e) {
 			tryToClose(rs);
 			tryToClose(ps);
@@ -1621,6 +1704,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public Collection<MessageId> getMessagesToSend(Connection txn, ContactId c,
 			int maxLength) throws DbException {
 		long now = clock.currentTimeMillis();
@@ -1628,10 +1712,13 @@ abstract class JdbcDatabase implements Database<Connection> {
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT length, m.messageId FROM messages AS m"
+					+ " JOIN groupVisibilities AS gv"
+					+ " ON m.groupId = gv.groupId"
 					+ " JOIN statuses AS s"
 					+ " ON m.messageId = s.messageId"
-					+ " WHERE contactId = ?"
-					+ " AND state = ? AND shared = TRUE AND raw IS NOT NULL"
+					+ " AND gv.contactId = s.contactId"
+					+ " WHERE gv.contactId = ? AND gv.shared = TRUE"
+					+ " AND state = ? AND m.shared = TRUE AND raw IS NOT NULL"
 					+ " AND seen = FALSE"
 					+ " AND expiry < ?"
 					+ " ORDER BY timestamp";
@@ -1650,7 +1737,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			}
 			rs.close();
 			ps.close();
-			return Collections.unmodifiableList(ids);
+			return ids;
 		} catch (SQLException e) {
 			tryToClose(rs);
 			tryToClose(ps);
@@ -1658,11 +1745,13 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public Collection<MessageId> getMessagesToValidate(Connection txn,
 			ClientId c) throws DbException {
 		return getMessagesInState(txn, c, UNKNOWN);
 	}
 
+	@Override
 	public Collection<MessageId> getPendingMessages(Connection txn,
 			ClientId c) throws DbException {
 		return getMessagesInState(txn, c, PENDING);
@@ -1684,7 +1773,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			while (rs.next()) ids.add(new MessageId(rs.getBytes(1)));
 			rs.close();
 			ps.close();
-			return Collections.unmodifiableList(ids);
+			return ids;
 		} catch (SQLException e) {
 			tryToClose(rs);
 			tryToClose(ps);
@@ -1692,6 +1781,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public Collection<MessageId> getMessagesToShare(
 			Connection txn, ClientId c) throws DbException {
 		PreparedStatement ps = null;
@@ -1713,7 +1803,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			while (rs.next()) ids.add(new MessageId(rs.getBytes(1)));
 			rs.close();
 			ps.close();
-			return Collections.unmodifiableList(ids);
+			return ids;
 		} catch (SQLException e) {
 			tryToClose(rs);
 			tryToClose(ps);
@@ -1721,6 +1811,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	@Nullable
 	public byte[] getRawMessage(Connection txn, MessageId m)
 			throws DbException {
@@ -1744,6 +1835,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public Collection<MessageId> getRequestedMessagesToSend(Connection txn,
 			ContactId c, int maxLength) throws DbException {
 		long now = clock.currentTimeMillis();
@@ -1751,10 +1843,13 @@ abstract class JdbcDatabase implements Database<Connection> {
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT length, m.messageId FROM messages AS m"
+					+ " JOIN groupVisibilities AS gv"
+					+ " ON m.groupId = gv.groupId"
 					+ " JOIN statuses AS s"
 					+ " ON m.messageId = s.messageId"
-					+ " WHERE contactId = ?"
-					+ " AND state = ? AND shared = TRUE AND raw IS NOT NULL"
+					+ " AND gv.contactId = s.contactId"
+					+ " WHERE gv.contactId = ? AND gv.shared = TRUE"
+					+ " AND state = ? AND m.shared = TRUE AND raw IS NOT NULL"
 					+ " AND seen = FALSE AND requested = TRUE"
 					+ " AND expiry < ?"
 					+ " ORDER BY timestamp";
@@ -1773,7 +1868,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			}
 			rs.close();
 			ps.close();
-			return Collections.unmodifiableList(ids);
+			return ids;
 		} catch (SQLException e) {
 			tryToClose(rs);
 			tryToClose(ps);
@@ -1781,6 +1876,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public Settings getSettings(Connection txn, String namespace)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -1802,6 +1898,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public Map<ContactId, TransportKeys> getTransportKeys(Connection txn,
 			TransportId t) throws DbException {
 		PreparedStatement ps = null;
@@ -1855,29 +1952,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			}
 			rs.close();
 			ps.close();
-			return Collections.unmodifiableMap(keys);
-		} catch (SQLException e) {
-			tryToClose(rs);
-			tryToClose(ps);
-			throw new DbException(e);
-		}
-	}
-
-	public Collection<ContactId> getVisibility(Connection txn, GroupId g)
-			throws DbException {
-		PreparedStatement ps = null;
-		ResultSet rs = null;
-		try {
-			String sql = "SELECT contactId FROM groupVisibilities"
-					+ " WHERE groupId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setBytes(1, g.getBytes());
-			rs = ps.executeQuery();
-			List<ContactId> visible = new ArrayList<ContactId>();
-			while (rs.next()) visible.add(new ContactId(rs.getInt(1)));
-			rs.close();
-			ps.close();
-			return Collections.unmodifiableList(visible);
+			return keys;
 		} catch (SQLException e) {
 			tryToClose(rs);
 			tryToClose(ps);
@@ -1885,6 +1960,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public void incrementStreamCounter(Connection txn, ContactId c,
 			TransportId t, long rotationPeriod) throws DbException {
 		PreparedStatement ps = null;
@@ -1904,6 +1980,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public void lowerAckFlag(Connection txn, ContactId c,
 			Collection<MessageId> acked) throws DbException {
 		PreparedStatement ps = null;
@@ -1930,6 +2007,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public void lowerRequestedFlag(Connection txn, ContactId c,
 			Collection<MessageId> requested) throws DbException {
 		PreparedStatement ps = null;
@@ -1956,11 +2034,13 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public void mergeGroupMetadata(Connection txn, GroupId g, Metadata meta)
 			throws DbException {
 		mergeMetadata(txn, g.getBytes(), meta, "groupMetadata", "groupId");
 	}
 
+	@Override
 	public void mergeMessageMetadata(Connection txn, MessageId m, Metadata meta)
 			throws DbException {
 		mergeMetadata(txn, m.getBytes(), meta, "messageMetadata", "messageId");
@@ -2041,6 +2121,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public void mergeSettings(Connection txn, Settings s, String namespace)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -2087,6 +2168,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public void raiseAckFlag(Connection txn, ContactId c, MessageId m)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -2105,6 +2187,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public void raiseRequestedFlag(Connection txn, ContactId c, MessageId m)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -2123,6 +2206,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public void raiseSeenFlag(Connection txn, ContactId c, MessageId m)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -2141,6 +2225,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public void removeContact(Connection txn, ContactId c)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -2157,6 +2242,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public void removeGroup(Connection txn, GroupId g) throws DbException {
 		PreparedStatement ps = null;
 		try {
@@ -2172,6 +2258,26 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
+	public void removeGroupVisibility(Connection txn, ContactId c, GroupId g)
+			throws DbException {
+		PreparedStatement ps = null;
+		try {
+			String sql = "DELETE FROM groupVisibilities"
+					+ " WHERE contactId = ? AND groupId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setInt(1, c.getInt());
+			ps.setBytes(2, g.getBytes());
+			int affected = ps.executeUpdate();
+			if (affected != 1) throw new DbStateException();
+			ps.close();
+		} catch (SQLException e) {
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
+	@Override
 	public void removeLocalAuthor(Connection txn, AuthorId a)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -2188,6 +2294,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public void removeMessage(Connection txn, MessageId m) throws DbException {
 		PreparedStatement ps = null;
 		try {
@@ -2203,6 +2310,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public boolean removeOfferedMessage(Connection txn, ContactId c,
 			MessageId m) throws DbException {
 		PreparedStatement ps = null;
@@ -2222,6 +2330,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public void removeOfferedMessages(Connection txn, ContactId c,
 			Collection<MessageId> requested) throws DbException {
 		PreparedStatement ps = null;
@@ -2246,6 +2355,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public void removeStatus(Connection txn, ContactId c, MessageId m)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -2264,6 +2374,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public void removeTransport(Connection txn, TransportId t)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -2280,17 +2391,18 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public void removeVisibility(Connection txn, ContactId c, GroupId g)
+	@Override
+	public void resetExpiryTime(Connection txn, ContactId c, MessageId m)
 			throws DbException {
 		PreparedStatement ps = null;
 		try {
-			String sql = "DELETE FROM groupVisibilities"
-					+ " WHERE contactId = ? AND groupId = ?";
+			String sql = "UPDATE statuses SET expiry = 0, txCount = 0"
+					+ " WHERE messageId = ? AND contactId = ?";
 			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			ps.setBytes(2, g.getBytes());
+			ps.setBytes(1, m.getBytes());
+			ps.setInt(2, c.getInt());
 			int affected = ps.executeUpdate();
-			if (affected != 1) throw new DbStateException();
+			if (affected < 0 || affected > 1) throw new DbStateException();
 			ps.close();
 		} catch (SQLException e) {
 			tryToClose(ps);
@@ -2298,14 +2410,14 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public void resetExpiryTime(Connection txn, ContactId c, MessageId m)
+	@Override
+	public void setContactVerified(Connection txn, ContactId c)
 			throws DbException {
 		PreparedStatement ps = null;
 		try {
-			String sql = "UPDATE statuses SET expiry = 0, txCount = 0"
-					+ " WHERE messageId = ? AND contactId = ?";
+			String sql = "UPDATE contacts SET verified = ? WHERE contactId = ?";
 			ps = txn.prepareStatement(sql);
-			ps.setBytes(1, m.getBytes());
+			ps.setBoolean(1, true);
 			ps.setInt(2, c.getInt());
 			int affected = ps.executeUpdate();
 			if (affected < 0 || affected > 1) throw new DbStateException();
@@ -2316,13 +2428,14 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public void setContactVerified(Connection txn, ContactId c)
+	@Override
+	public void setContactActive(Connection txn, ContactId c, boolean active)
 			throws DbException {
 		PreparedStatement ps = null;
 		try {
-			String sql = "UPDATE contacts SET verified = ? WHERE contactId = ?";
+			String sql = "UPDATE contacts SET active = ? WHERE contactId = ?";
 			ps = txn.prepareStatement(sql);
-			ps.setBoolean(1, true);
+			ps.setBoolean(1, active);
 			ps.setInt(2, c.getInt());
 			int affected = ps.executeUpdate();
 			if (affected < 0 || affected > 1) throw new DbStateException();
@@ -2333,14 +2446,17 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public void setContactActive(Connection txn, ContactId c, boolean active)
-			throws DbException {
+	@Override
+	public void setGroupVisibility(Connection txn, ContactId c, GroupId g,
+			boolean shared) throws DbException {
 		PreparedStatement ps = null;
 		try {
-			String sql = "UPDATE contacts SET active = ? WHERE contactId = ?";
+			String sql = "UPDATE groupVisibilities SET shared = ?"
+					+ " WHERE contactId = ? AND groupId = ?";
 			ps = txn.prepareStatement(sql);
-			ps.setBoolean(1, active);
+			ps.setBoolean(1, shared);
 			ps.setInt(2, c.getInt());
+			ps.setBytes(3, g.getBytes());
 			int affected = ps.executeUpdate();
 			if (affected < 0 || affected > 1) throw new DbStateException();
 			ps.close();
@@ -2350,6 +2466,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public void setMessageShared(Connection txn, MessageId m) throws DbException {
 		PreparedStatement ps = null;
 		try {
@@ -2366,6 +2483,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public void setMessageState(Connection txn, MessageId m, State state)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -2383,6 +2501,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public void setReorderingWindow(Connection txn, ContactId c, TransportId t,
 			long rotationPeriod, long base, byte[] bitmap) throws DbException {
 		PreparedStatement ps = null;
@@ -2404,6 +2523,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public void updateExpiryTime(Connection txn, ContactId c, MessageId m,
 			int maxLatency) throws DbException {
 		PreparedStatement ps = null;
@@ -2437,6 +2557,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	@Override
 	public void updateTransportKeys(Connection txn,
 			Map<ContactId, TransportKeys> keys) throws DbException {
 		PreparedStatement ps = null;
diff --git a/briar-core/src/org/briarproject/introduction/IntroductionManagerImpl.java b/briar-core/src/org/briarproject/introduction/IntroductionManagerImpl.java
index e59f34e8498ac4de2b74db755a520fd929e0fa42..e3f7981a5d16ed3485d6e12f1967e5d59e736dd3 100644
--- a/briar-core/src/org/briarproject/introduction/IntroductionManagerImpl.java
+++ b/briar-core/src/org/briarproject/introduction/IntroductionManagerImpl.java
@@ -73,6 +73,7 @@ import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT
 import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ACK;
 import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST;
 import static org.briarproject.api.introduction.IntroductionConstants.TYPE_RESPONSE;
+import static org.briarproject.api.sync.Group.Visibility.SHARED;
 import static org.briarproject.clients.BdfConstants.MSG_KEY_READ;
 
 @NotNullByDefault
@@ -116,7 +117,7 @@ class IntroductionManagerImpl extends ConversationClientImpl
 			if (db.containsGroup(txn, g.getId())) return;
 			// Store the group and share it with the contact
 			db.addGroup(txn, g);
-			db.setVisibleToContact(txn, c.getId(), g.getId(), true);
+			db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED);
 			// Attach the contact ID to the group
 			BdfDictionary gm = new BdfDictionary();
 			gm.put(CONTACT, c.getId().getInt());
diff --git a/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java b/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java
index 233682600d938251ffc597a747f5be787fedc192..90fbe5c79f5e6887aae9a33d2cf8325bb7d04f72 100644
--- a/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java
+++ b/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java
@@ -33,6 +33,7 @@ import java.util.Map;
 
 import javax.inject.Inject;
 
+import static org.briarproject.api.sync.Group.Visibility.SHARED;
 import static org.briarproject.clients.BdfConstants.MSG_KEY_READ;
 
 @NotNullByDefault
@@ -64,7 +65,7 @@ class MessagingManagerImpl extends ConversationClientImpl
 			if (db.containsGroup(txn, g.getId())) return;
 			// Store the group and share it with the contact
 			db.addGroup(txn, g);
-			db.setVisibleToContact(txn, c.getId(), g.getId(), true);
+			db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED);
 			// Attach the contact ID to the group
 			BdfDictionary d = new BdfDictionary();
 			d.put("contactId", c.getId().getInt());
diff --git a/briar-core/src/org/briarproject/privategroup/invitation/AbstractProtocolEngine.java b/briar-core/src/org/briarproject/privategroup/invitation/AbstractProtocolEngine.java
index ddf3b5ae20c304b3dfae078dc27461e3651e902a..6eb230247f248f7475d2ca113757051357fa2911 100644
--- a/briar-core/src/org/briarproject/privategroup/invitation/AbstractProtocolEngine.java
+++ b/briar-core/src/org/briarproject/privategroup/invitation/AbstractProtocolEngine.java
@@ -18,6 +18,7 @@ import org.briarproject.api.privategroup.PrivateGroup;
 import org.briarproject.api.privategroup.PrivateGroupFactory;
 import org.briarproject.api.privategroup.PrivateGroupManager;
 import org.briarproject.api.sync.Group;
+import org.briarproject.api.sync.Group.Visibility;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
@@ -90,11 +91,10 @@ abstract class AbstractProtocolEngine<S extends Session>
 		return dependency.equals(expected);
 	}
 
-	void syncPrivateGroupWithContact(Transaction txn, S session, boolean sync)
+	void setPrivateGroupVisibility(Transaction txn, S session, Visibility v)
 			throws DbException, FormatException {
 		ContactId contactId = getContactId(txn, session.getContactGroupId());
-		db.setVisibleToContact(txn, contactId, session.getPrivateGroupId(),
-				sync);
+		db.setGroupVisibility(txn, contactId, session.getPrivateGroupId(), v);
 	}
 
 	Message sendInviteMessage(Transaction txn, S session,
diff --git a/briar-core/src/org/briarproject/privategroup/invitation/CreatorProtocolEngine.java b/briar-core/src/org/briarproject/privategroup/invitation/CreatorProtocolEngine.java
index f58630c4c8b73c6a00d439648c945ae241ef96fb..26f1715797a515b0ad8e156dbd86a8ceead788be 100644
--- a/briar-core/src/org/briarproject/privategroup/invitation/CreatorProtocolEngine.java
+++ b/briar-core/src/org/briarproject/privategroup/invitation/CreatorProtocolEngine.java
@@ -22,6 +22,8 @@ import org.briarproject.api.system.Clock;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
+import static org.briarproject.api.sync.Group.Visibility.INVISIBLE;
+import static org.briarproject.api.sync.Group.Visibility.SHARED;
 import static org.briarproject.privategroup.invitation.CreatorState.DISSOLVED;
 import static org.briarproject.privategroup.invitation.CreatorState.ERROR;
 import static org.briarproject.privategroup.invitation.CreatorState.INVITED;
@@ -158,8 +160,8 @@ class CreatorProtocolEngine extends AbstractProtocolEngine<CreatorSession> {
 	private CreatorSession onLocalLeave(Transaction txn, CreatorSession s)
 			throws DbException {
 		try {
-			// Stop syncing the private group with the contact
-			syncPrivateGroupWithContact(txn, s, false);
+			// Make the private group invisible to the contact
+			setPrivateGroupVisibility(txn, s, INVISIBLE);
 		} catch (FormatException e) {
 			throw new DbException(e); // Invalid group metadata
 		}
@@ -183,8 +185,8 @@ class CreatorProtocolEngine extends AbstractProtocolEngine<CreatorSession> {
 		// Track the message
 		messageTracker.trackMessage(txn, m.getContactGroupId(),
 				m.getTimestamp(), false);
-		// Start syncing the private group with the contact
-		syncPrivateGroupWithContact(txn, s, true);
+		// Share the private group with the contact
+		setPrivateGroupVisibility(txn, s, SHARED);
 		// Broadcast an event
 		ContactId contactId = getContactId(txn, m.getContactGroupId());
 		txn.attach(new GroupInvitationResponseReceivedEvent(contactId,
@@ -224,8 +226,8 @@ class CreatorProtocolEngine extends AbstractProtocolEngine<CreatorSession> {
 		// The dependency, if any, must be the last remote message
 		if (!isValidDependency(s, m.getPreviousMessageId()))
 			return abort(txn, s);
-		// Stop syncing the private group with the contact
-		syncPrivateGroupWithContact(txn, s, false);
+		// Make the private group invisible to the contact
+		setPrivateGroupVisibility(txn, s, INVISIBLE);
 		// Move to the INVITEE_LEFT state
 		return new CreatorSession(s.getContactGroupId(), s.getPrivateGroupId(),
 				s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(),
@@ -236,9 +238,9 @@ class CreatorProtocolEngine extends AbstractProtocolEngine<CreatorSession> {
 			throws DbException, FormatException {
 		// If the session has already been aborted, do nothing
 		if (s.getState() == ERROR) return s;
-		// If we subscribe, stop syncing the private group with the contact
+		// If we subscribe, make the private group invisible to the contact
 		if (isSubscribedPrivateGroup(txn, s.getPrivateGroupId()))
-			syncPrivateGroupWithContact(txn, s, false);
+			setPrivateGroupVisibility(txn, s, INVISIBLE);
 		// Send an ABORT message
 		Message sent = sendAbortMessage(txn, s);
 		// Move to the ERROR state
diff --git a/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationManagerImpl.java b/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationManagerImpl.java
index d90668e6d39f62fb4029f7d7c7204a0c30a85492..5be3b4573143e41cb97234ffbebcba73e9ca8723 100644
--- a/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationManagerImpl.java
+++ b/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationManagerImpl.java
@@ -45,6 +45,7 @@ import java.util.Map.Entry;
 import javax.annotation.concurrent.Immutable;
 import javax.inject.Inject;
 
+import static org.briarproject.api.sync.Group.Visibility.SHARED;
 import static org.briarproject.privategroup.invitation.CreatorState.START;
 import static org.briarproject.privategroup.invitation.GroupInvitationConstants.GROUP_KEY_CONTACT_ID;
 import static org.briarproject.privategroup.invitation.MessageType.ABORT;
@@ -110,7 +111,7 @@ class GroupInvitationManagerImpl extends ConversationClientImpl
 		if (db.containsGroup(txn, g.getId())) return;
 		// Store the group and share it with the contact
 		db.addGroup(txn, g);
-		db.setVisibleToContact(txn, c.getId(), g.getId(), true);
+		db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED);
 		// Attach the contact ID to the group
 		BdfDictionary meta = new BdfDictionary();
 		meta.put(GROUP_KEY_CONTACT_ID, c.getId().getInt());
diff --git a/briar-core/src/org/briarproject/privategroup/invitation/InviteeProtocolEngine.java b/briar-core/src/org/briarproject/privategroup/invitation/InviteeProtocolEngine.java
index 60579d46f5c4339a441092788c5d418a2c535664..4398badbf23598d191f182e9971a6ec37113fb1f 100644
--- a/briar-core/src/org/briarproject/privategroup/invitation/InviteeProtocolEngine.java
+++ b/briar-core/src/org/briarproject/privategroup/invitation/InviteeProtocolEngine.java
@@ -25,6 +25,8 @@ import org.briarproject.api.system.Clock;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
+import static org.briarproject.api.sync.Group.Visibility.INVISIBLE;
+import static org.briarproject.api.sync.Group.Visibility.SHARED;
 import static org.briarproject.privategroup.invitation.InviteeState.DISSOLVED;
 import static org.briarproject.privategroup.invitation.InviteeState.ERROR;
 import static org.briarproject.privategroup.invitation.InviteeState.INVITED;
@@ -157,8 +159,8 @@ class InviteeProtocolEngine extends AbstractProtocolEngine<InviteeSession> {
 		try {
 			// Subscribe to the private group
 			subscribeToPrivateGroup(txn, inviteId);
-			// Start syncing the private group with the contact
-			syncPrivateGroupWithContact(txn, s, true);
+			// Share the private group with the contact
+			setPrivateGroupVisibility(txn, s, SHARED);
 		} catch (FormatException e) {
 			throw new DbException(e); // Invalid group metadata
 		}
@@ -228,8 +230,8 @@ class InviteeProtocolEngine extends AbstractProtocolEngine<InviteeSession> {
 		if (!isValidDependency(s, m.getPreviousMessageId()))
 			return abort(txn, s);
 		try {
-			// Stop syncing the private group with the contact
-			syncPrivateGroupWithContact(txn, s, false);
+			// Make the private group invisible to the contact
+			setPrivateGroupVisibility(txn, s, INVISIBLE);
 		} catch (FormatException e) {
 			throw new DbException(e); // Invalid group metadata
 		}
@@ -247,9 +249,9 @@ class InviteeProtocolEngine extends AbstractProtocolEngine<InviteeSession> {
 		if (s.getState() == ERROR) return s;
 		// Mark any invite messages in the session unavailable to answer
 		markInvitesUnavailableToAnswer(txn, s);
-		// Stop syncing the private group with the contact, if we subscribe
+		// If we subscribe, make the private group invisible to the contact
 		if (isSubscribedPrivateGroup(txn, s.getPrivateGroupId()))
-			syncPrivateGroupWithContact(txn, s, false);
+			setPrivateGroupVisibility(txn, s, INVISIBLE);
 		// Send an ABORT message
 		Message sent = sendAbortMessage(txn, s);
 		// Move to the ERROR state
diff --git a/briar-core/src/org/briarproject/privategroup/invitation/PeerProtocolEngine.java b/briar-core/src/org/briarproject/privategroup/invitation/PeerProtocolEngine.java
index d6674f841227097c2bd695f35810c91ae394a25c..06b019649ea27b23d6f3f6e0baedc0f5cf1cc5fc 100644
--- a/briar-core/src/org/briarproject/privategroup/invitation/PeerProtocolEngine.java
+++ b/briar-core/src/org/briarproject/privategroup/invitation/PeerProtocolEngine.java
@@ -20,6 +20,8 @@ import org.briarproject.api.system.Clock;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
+import static org.briarproject.api.sync.Group.Visibility.INVISIBLE;
+import static org.briarproject.api.sync.Group.Visibility.SHARED;
 import static org.briarproject.privategroup.invitation.PeerState.AWAIT_MEMBER;
 import static org.briarproject.privategroup.invitation.PeerState.BOTH_JOINED;
 import static org.briarproject.privategroup.invitation.PeerState.ERROR;
@@ -178,8 +180,8 @@ class PeerProtocolEngine extends AbstractProtocolEngine<PeerSession> {
 		// Send a JOIN message
 		Message sent = sendJoinMessage(txn, s, false);
 		try {
-			// Start syncing the private group with the contact
-			syncPrivateGroupWithContact(txn, s, true);
+			// Share the private group with the contact
+			setPrivateGroupVisibility(txn, s, SHARED);
 		} catch (FormatException e) {
 			throw new DbException(e); // Invalid group metadata
 		}
@@ -195,8 +197,8 @@ class PeerProtocolEngine extends AbstractProtocolEngine<PeerSession> {
 		// Send a LEAVE message
 		Message sent = sendLeaveMessage(txn, s, false);
 		try {
-			// Stop syncing the private group with the contact
-			syncPrivateGroupWithContact(txn, s, false);
+			// Make the private group invisible to the contact
+			setPrivateGroupVisibility(txn, s, INVISIBLE);
 		} catch (FormatException e) {
 			throw new DbException(e); // Invalid group metadata
 		}
@@ -228,8 +230,8 @@ class PeerProtocolEngine extends AbstractProtocolEngine<PeerSession> {
 		// Send a JOIN message
 		Message sent = sendJoinMessage(txn, s, false);
 		try {
-			// Start syncing the private group with the contact
-			syncPrivateGroupWithContact(txn, s, true);
+			// Share the private group with the contact
+			setPrivateGroupVisibility(txn, s, SHARED);
 		} catch (FormatException e) {
 			throw new DbException(e); // Invalid group metadata
 		}
@@ -263,8 +265,8 @@ class PeerProtocolEngine extends AbstractProtocolEngine<PeerSession> {
 			return abort(txn, s);
 		// Send a JOIN message
 		Message sent = sendJoinMessage(txn, s, false);
-		// Start syncing the private group with the contact
-		syncPrivateGroupWithContact(txn, s, true);
+		// Share the private group with the contact
+		setPrivateGroupVisibility(txn, s, SHARED);
 		// Mark the relationship visible to the group, revealed by contact
 		relationshipRevealed(txn, s, true);
 		// Move to the BOTH_JOINED state
@@ -277,8 +279,8 @@ class PeerProtocolEngine extends AbstractProtocolEngine<PeerSession> {
 		// The dependency, if any, must be the last remote message
 		if (!isValidDependency(s, m.getPreviousMessageId()))
 			return abort(txn, s);
-		// Start syncing the private group with the contact
-		syncPrivateGroupWithContact(txn, s, true);
+		// Share the private group with the contact
+		setPrivateGroupVisibility(txn, s, SHARED);
 		// Mark the relationship visible to the group, revealed by us
 		relationshipRevealed(txn, s, false);
 		// Move to the BOTH_JOINED state
@@ -314,8 +316,8 @@ class PeerProtocolEngine extends AbstractProtocolEngine<PeerSession> {
 		// The dependency, if any, must be the last remote message
 		if (!isValidDependency(s, m.getPreviousMessageId()))
 			return abort(txn, s);
-		// Stop syncing the private group with the contact
-		syncPrivateGroupWithContact(txn, s, false);
+		// Make the private group invisible to the contact
+		setPrivateGroupVisibility(txn, s, INVISIBLE);
 		// Move to the LOCAL_JOINED state
 		return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
 				s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(),
@@ -326,9 +328,9 @@ class PeerProtocolEngine extends AbstractProtocolEngine<PeerSession> {
 			throws DbException, FormatException {
 		// If the session has already been aborted, do nothing
 		if (s.getState() == ERROR) return s;
-		// Stop syncing the private group with the contact, if we subscribe
+		// If we subscribe, make the private group invisible to the contact
 		if (isSubscribedPrivateGroup(txn, s.getPrivateGroupId()))
-			syncPrivateGroupWithContact(txn, s, false);
+			setPrivateGroupVisibility(txn, s, INVISIBLE);
 		// Send an ABORT message
 		Message sent = sendAbortMessage(txn, s);
 		// Move to the ERROR state
diff --git a/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java b/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java
index b0900795e1204a754139f0ae9cd26fd115e28cf7..405d252b927027ce845ad401f50e93e6388b7c92 100644
--- a/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java
+++ b/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java
@@ -29,6 +29,8 @@ import java.util.Map.Entry;
 
 import javax.inject.Inject;
 
+import static org.briarproject.api.sync.Group.Visibility.SHARED;
+
 class TransportPropertyManagerImpl implements TransportPropertyManager,
 		Client, AddContactHook, RemoveContactHook {
 
@@ -64,7 +66,7 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 		if (db.containsGroup(txn, g.getId())) return;
 		// Store the group and share it with the contact
 		db.addGroup(txn, g);
-		db.setVisibleToContact(txn, c.getId(), g.getId(), true);
+		db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED);
 		// Copy the latest local properties into the group
 		Map<TransportId, TransportProperties> local = getLocalProperties(txn);
 		for (Entry<TransportId, TransportProperties> e : local.entrySet()) {
diff --git a/briar-core/src/org/briarproject/sharing/SharingManagerImpl.java b/briar-core/src/org/briarproject/sharing/SharingManagerImpl.java
index 95697ffa40a493b0cbcb678c0ff0e39f0f4edc0d..2768d4a7eda5e88b1640c8f0e7452820980a8535 100644
--- a/briar-core/src/org/briarproject/sharing/SharingManagerImpl.java
+++ b/briar-core/src/org/briarproject/sharing/SharingManagerImpl.java
@@ -87,6 +87,8 @@ import static org.briarproject.api.sharing.SharingConstants.TO_BE_SHARED_BY_US;
 import static org.briarproject.api.sharing.SharingConstants.TYPE;
 import static org.briarproject.api.sharing.SharingMessage.BaseMessage;
 import static org.briarproject.api.sharing.SharingMessage.Invitation;
+import static org.briarproject.api.sync.Group.Visibility.INVISIBLE;
+import static org.briarproject.api.sync.Group.Visibility.SHARED;
 import static org.briarproject.clients.BdfConstants.MSG_KEY_READ;
 import static org.briarproject.sharing.InviteeSessionState.State.AWAIT_LOCAL_RESPONSE;
 
@@ -159,7 +161,7 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 			if (db.containsGroup(txn, g.getId())) return;
 			// Store the group and share it with the contact
 			db.addGroup(txn, g);
-			db.setVisibleToContact(txn, c.getId(), g.getId(), true);
+			db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED);
 			// Attach the contact ID to the group
 			BdfDictionary meta = new BdfDictionary();
 			meta.put(CONTACT_ID, c.getId().getInt());
@@ -908,20 +910,20 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 			// TODO we might want to call the add() method of the respective
 			//      manager here, because blogs add a description for example
 			db.addGroup(txn, f.getGroup());
-			db.setVisibleToContact(txn, contactId, f.getId(), true);
+			db.setGroupVisibility(txn, contactId, f.getId(), SHARED);
 		} else if (task == TASK_ADD_SHAREABLE_TO_LIST_TO_BE_SHARED_BY_US) {
 			addToList(txn, groupId, TO_BE_SHARED_BY_US, f);
 		} else if (task == TASK_REMOVE_SHAREABLE_FROM_LIST_TO_BE_SHARED_BY_US) {
 			removeFromList(txn, groupId, TO_BE_SHARED_BY_US, f);
 		} else if (task == TASK_SHARE_SHAREABLE) {
-			db.setVisibleToContact(txn, contactId, f.getId(), true);
+			db.setGroupVisibility(txn, contactId, f.getId(), SHARED);
 			removeFromList(txn, groupId, TO_BE_SHARED_BY_US, f);
 			addToList(txn, groupId, SHARED_BY_US, f);
 		} else if (task == TASK_UNSHARE_SHAREABLE_SHARED_BY_US) {
-			db.setVisibleToContact(txn, contactId, f.getId(), false);
+			db.setGroupVisibility(txn, contactId, f.getId(), INVISIBLE);
 			removeFromList(txn, groupId, SHARED_BY_US, f);
 		} else if (task == TASK_UNSHARE_SHAREABLE_SHARED_WITH_US) {
-			db.setVisibleToContact(txn, contactId, f.getId(), false);
+			db.setGroupVisibility(txn, contactId, f.getId(), INVISIBLE);
 			removeFromList(txn, groupId, SHARED_WITH_US, f);
 		}
 	}
diff --git a/briar-tests/src/org/briarproject/blogs/BlogManagerImplTest.java b/briar-tests/src/org/briarproject/blogs/BlogManagerImplTest.java
index 552ebaf37bc8c9901bd2c5c6681cae394cb708e8..7d661e8ac20f5c717f83c2772e2318f6283a20de 100644
--- a/briar-tests/src/org/briarproject/blogs/BlogManagerImplTest.java
+++ b/briar-tests/src/org/briarproject/blogs/BlogManagerImplTest.java
@@ -49,6 +49,7 @@ import static org.briarproject.api.blogs.BlogConstants.KEY_TYPE;
 import static org.briarproject.api.blogs.MessageType.POST;
 import static org.briarproject.api.identity.Author.Status.VERIFIED;
 import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
+import static org.briarproject.api.sync.Group.Visibility.SHARED;
 import static org.briarproject.blogs.BlogManagerImpl.CLIENT_ID;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -102,25 +103,18 @@ public class BlogManagerImplTest extends BriarTestCase {
 			will(returnValue(blog1.getAuthor()));
 			oneOf(blogFactory).createBlog(blog1.getAuthor());
 			will(returnValue(blog1));
-			oneOf(db).containsGroup(txn, blog1.getId());
-			will(returnValue(false));
 			oneOf(db).addGroup(txn, blog1.getGroup());
-			oneOf(db).getContacts(txn, blog1.getAuthor().getId());
-			will(returnValue(contactIds));
-			oneOf(db).setVisibleToContact(txn, contactId, blog1.getId(), true);
 			oneOf(db).getContacts(txn);
 			will(returnValue(contacts));
 			oneOf(blogFactory).createBlog(blog2.getAuthor());
 			will(returnValue(blog2));
-			oneOf(db).containsGroup(txn, blog2.getId());
-			will(returnValue(false));
 			oneOf(db).addGroup(txn, blog2.getGroup());
-			oneOf(db).setVisibleToContact(txn, contactId, blog2.getId(), true);
-			oneOf(db).getLocalAuthor(txn, blog1.getAuthor().getId());
+			oneOf(db).setGroupVisibility(txn, contactId, blog2.getId(), SHARED);
+			oneOf(identityManager).getLocalAuthor(txn);
 			will(returnValue(blog1.getAuthor()));
 			oneOf(blogFactory).createBlog(blog1.getAuthor());
 			will(returnValue(blog1));
-			oneOf(db).setVisibleToContact(txn, contactId, blog1.getId(), true);
+			oneOf(db).setGroupVisibility(txn, contactId, blog1.getId(), SHARED);
 		}});
 
 		blogManager.createLocalState(txn);
diff --git a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java
index 44c62675cb207a2352b6cfd9fb8bd4e30f0f90b5..d61f0cce936f28491e9221121962454eb3b5a83a 100644
--- a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java
+++ b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java
@@ -60,6 +60,9 @@ import java.util.Collections;
 import java.util.Map;
 
 import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
+import static org.briarproject.api.sync.Group.Visibility.INVISIBLE;
+import static org.briarproject.api.sync.Group.Visibility.SHARED;
+import static org.briarproject.api.sync.Group.Visibility.VISIBLE;
 import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
 import static org.briarproject.api.sync.ValidationManager.State.UNKNOWN;
 import static org.briarproject.api.transport.TransportConstants.REORDERING_WINDOW_SIZE;
@@ -170,7 +173,7 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			// removeGroup()
 			oneOf(database).containsGroup(txn, groupId);
 			will(returnValue(true));
-			oneOf(database).getVisibility(txn, groupId);
+			oneOf(database).getGroupVisibility(txn, groupId);
 			will(returnValue(Collections.emptyList()));
 			oneOf(database).removeGroup(txn, groupId);
 			oneOf(eventBus).broadcast(with(any(GroupRemovedEvent.class)));
@@ -267,7 +270,7 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			will(returnValue(false));
 			oneOf(database).addMessage(txn, message, DELIVERED, true);
 			oneOf(database).mergeMessageMetadata(txn, messageId, metadata);
-			oneOf(database).getVisibility(txn, groupId);
+			oneOf(database).getGroupVisibility(txn, groupId);
 			will(returnValue(Collections.singletonList(contactId)));
 			oneOf(database).removeOfferedMessage(txn, contactId, messageId);
 			will(returnValue(false));
@@ -403,7 +406,7 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 
 		transaction = db.startTransaction(false);
 		try {
-			db.isVisibleToContact(transaction, contactId, groupId);
+			db.getGroupVisibility(transaction, contactId, groupId);
 			fail();
 		} catch (NoSuchContactException expected) {
 			// Expected
@@ -487,7 +490,7 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 
 		transaction = db.startTransaction(false);
 		try {
-			db.setVisibleToContact(transaction, contactId, groupId, true);
+			db.setGroupVisibility(transaction, contactId, groupId, SHARED);
 			fail();
 		} catch (NoSuchContactException expected) {
 			// Expected
@@ -560,14 +563,13 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		final EventBus eventBus = context.mock(EventBus.class);
 		context.checking(new Expectations() {{
 			// Check whether the group is in the DB (which it's not)
-			exactly(9).of(database).startTransaction();
+			exactly(8).of(database).startTransaction();
 			will(returnValue(txn));
-			exactly(9).of(database).containsGroup(txn, groupId);
+			exactly(8).of(database).containsGroup(txn, groupId);
 			will(returnValue(false));
-			exactly(9).of(database).abortTransaction(txn);
-			// This is needed for getMessageStatus(), isVisibleToContact(), and
-			// setVisibleToContact() to proceed
-			exactly(3).of(database).containsContact(txn, contactId);
+			exactly(8).of(database).abortTransaction(txn);
+			// This is needed for getMessageStatus() and setGroupVisibility()
+			exactly(2).of(database).containsContact(txn, contactId);
 			will(returnValue(true));
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
@@ -623,16 +625,6 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			db.endTransaction(transaction);
 		}
 
-		transaction = db.startTransaction(false);
-		try {
-			db.isVisibleToContact(transaction, contactId, groupId);
-			fail();
-		} catch (NoSuchGroupException expected) {
-			// Expected
-		} finally {
-			db.endTransaction(transaction);
-		}
-
 		transaction = db.startTransaction(false);
 		try {
 			db.mergeGroupMetadata(transaction, groupId, metadata);
@@ -655,7 +647,7 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 
 		transaction = db.startTransaction(false);
 		try {
-			db.setVisibleToContact(transaction, contactId, groupId, true);
+			db.setGroupVisibility(transaction, contactId, groupId, SHARED);
 			fail();
 		} catch (NoSuchGroupException expected) {
 			// Expected
@@ -924,6 +916,7 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		Transaction transaction = db.startTransaction(false);
 		try {
 			Ack a = db.generateAck(transaction, contactId, 123);
+			assertNotNull(a);
 			assertEquals(messagesToAck, a.getMessageIds());
 			db.commitTransaction(transaction);
 		} finally {
@@ -1005,6 +998,7 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		Transaction transaction = db.startTransaction(false);
 		try {
 			Offer o = db.generateOffer(transaction, contactId, 123, maxLatency);
+			assertNotNull(o);
 			assertEquals(ids, o.getMessageIds());
 			db.commitTransaction(transaction);
 		} finally {
@@ -1039,6 +1033,7 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		Transaction transaction = db.startTransaction(false);
 		try {
 			Request r = db.generateRequest(transaction, contactId, 123);
+			assertNotNull(r);
 			assertEquals(ids, r.getMessageIds());
 			db.commitTransaction(transaction);
 		} finally {
@@ -1139,12 +1134,12 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			// First time
 			oneOf(database).containsContact(txn, contactId);
 			will(returnValue(true));
-			oneOf(database).containsVisibleGroup(txn, contactId, groupId);
-			will(returnValue(true));
+			oneOf(database).getGroupVisibility(txn, contactId, groupId);
+			will(returnValue(VISIBLE));
 			oneOf(database).containsMessage(txn, messageId);
 			will(returnValue(false));
 			oneOf(database).addMessage(txn, message, UNKNOWN, false);
-			oneOf(database).getVisibility(txn, groupId);
+			oneOf(database).getGroupVisibility(txn, groupId);
 			will(returnValue(Collections.singletonList(contactId)));
 			oneOf(database).removeOfferedMessage(txn, contactId, messageId);
 			will(returnValue(false));
@@ -1152,8 +1147,8 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			// Second time
 			oneOf(database).containsContact(txn, contactId);
 			will(returnValue(true));
-			oneOf(database).containsVisibleGroup(txn, contactId, groupId);
-			will(returnValue(true));
+			oneOf(database).getGroupVisibility(txn, contactId, groupId);
+			will(returnValue(VISIBLE));
 			oneOf(database).containsMessage(txn, messageId);
 			will(returnValue(true));
 			oneOf(database).raiseSeenFlag(txn, contactId, messageId);
@@ -1195,8 +1190,8 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			will(returnValue(true));
 			oneOf(database).containsMessage(txn, messageId);
 			will(returnValue(true));
-			oneOf(database).containsVisibleGroup(txn, contactId, groupId);
-			will(returnValue(true));
+			oneOf(database).getGroupVisibility(txn, contactId, groupId);
+			will(returnValue(VISIBLE));
 			// The message wasn't stored but it must still be acked
 			oneOf(database).raiseSeenFlag(txn, contactId, messageId);
 			oneOf(database).raiseAckFlag(txn, contactId, messageId);
@@ -1230,8 +1225,8 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			will(returnValue(txn));
 			oneOf(database).containsContact(txn, contactId);
 			will(returnValue(true));
-			oneOf(database).containsVisibleGroup(txn, contactId, groupId);
-			will(returnValue(false));
+			oneOf(database).getGroupVisibility(txn, contactId, groupId);
+			will(returnValue(INVISIBLE));
 			oneOf(database).commitTransaction(txn);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
@@ -1350,9 +1345,9 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			will(returnValue(true));
 			oneOf(database).containsGroup(txn, groupId);
 			will(returnValue(true));
-			oneOf(database).containsVisibleGroup(txn, contactId, groupId);
-			will(returnValue(false)); // Not yet visible
-			oneOf(database).addVisibility(txn, contactId, groupId);
+			oneOf(database).getGroupVisibility(txn, contactId, groupId);
+			will(returnValue(INVISIBLE)); // Not yet visible
+			oneOf(database).addGroupVisibility(txn, contactId, groupId, false);
 			oneOf(database).getMessageIds(txn, groupId);
 			will(returnValue(Collections.singletonList(messageId)));
 			oneOf(database).removeOfferedMessage(txn, contactId, messageId);
@@ -1367,7 +1362,7 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 
 		Transaction transaction = db.startTransaction(false);
 		try {
-			db.setVisibleToContact(transaction, contactId, groupId, true);
+			db.setGroupVisibility(transaction, contactId, groupId, VISIBLE);
 			db.commitTransaction(transaction);
 		} finally {
 			db.endTransaction(transaction);
@@ -1391,8 +1386,8 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			will(returnValue(true));
 			oneOf(database).containsGroup(txn, groupId);
 			will(returnValue(true));
-			oneOf(database).containsVisibleGroup(txn, contactId, groupId);
-			will(returnValue(true)); // Already visible
+			oneOf(database).getGroupVisibility(txn, contactId, groupId);
+			will(returnValue(VISIBLE)); // Already visible
 			oneOf(database).commitTransaction(txn);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
@@ -1400,7 +1395,7 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 
 		Transaction transaction = db.startTransaction(false);
 		try {
-			db.setVisibleToContact(transaction, contactId, groupId, true);
+			db.setGroupVisibility(transaction, contactId, groupId, VISIBLE);
 			db.commitTransaction(transaction);
 		} finally {
 			db.endTransaction(transaction);
@@ -1666,7 +1661,7 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			oneOf(database).containsMessage(txn, messageId);
 			will(returnValue(false));
 			oneOf(database).addMessage(txn, message, DELIVERED, true);
-			oneOf(database).getVisibility(txn, groupId);
+			oneOf(database).getGroupVisibility(txn, groupId);
 			will(returnValue(Collections.singletonList(contactId)));
 			oneOf(database).mergeMessageMetadata(txn, messageId, metadata);
 			oneOf(database).removeOfferedMessage(txn, contactId, messageId);
diff --git a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java
index e5f368a7c28b9ccb62ebe75487199175847f245e..0b092c3d9a494abe0ebcac3003e71bcd230c3623 100644
--- a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java
+++ b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java
@@ -45,6 +45,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
 import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.briarproject.api.db.Metadata.REMOVE;
 import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
+import static org.briarproject.api.sync.Group.Visibility.INVISIBLE;
+import static org.briarproject.api.sync.Group.Visibility.SHARED;
+import static org.briarproject.api.sync.Group.Visibility.VISIBLE;
 import static org.briarproject.api.sync.SyncConstants.MAX_MESSAGE_LENGTH;
 import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
 import static org.briarproject.api.sync.ValidationManager.State.INVALID;
@@ -171,12 +174,12 @@ public class H2DatabaseTest extends BriarTestCase {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add a contact, a group and a message
+		// Add a contact, a shared group and a shared message
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
 				true, true));
 		db.addGroup(txn, group);
-		db.addVisibility(txn, contactId, groupId);
+		db.addGroupVisibility(txn, contactId, groupId, true);
 		db.addMessage(txn, message, DELIVERED, true);
 
 		// The message has no status yet, so it should not be sendable
@@ -209,12 +212,12 @@ public class H2DatabaseTest extends BriarTestCase {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add a contact, a group and an unvalidated message
+		// Add a contact, a shared group and a shared but unvalidated message
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
 				true, true));
 		db.addGroup(txn, group);
-		db.addVisibility(txn, contactId, groupId);
+		db.addGroupVisibility(txn, contactId, groupId, true);
 		db.addMessage(txn, message, UNKNOWN, true);
 		db.addStatus(txn, contactId, messageId, false, false);
 
@@ -250,17 +253,69 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.close();
 	}
 
+	@Test
+	public void testSendableMessagesMustHaveSharedGroup() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a contact, an invisible group and a shared message
+		db.addLocalAuthor(txn, localAuthor);
+		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
+				true, true));
+		db.addGroup(txn, group);
+		db.addMessage(txn, message, DELIVERED, true);
+		db.addStatus(txn, contactId, messageId, false, false);
+
+		// The group is invisible, so the message should not be sendable
+		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
+				ONE_MEGABYTE);
+		assertTrue(ids.isEmpty());
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertTrue(ids.isEmpty());
+
+		// Making the group visible should not make the message sendable
+		db.addGroupVisibility(txn, contactId, groupId, false);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		assertTrue(ids.isEmpty());
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertTrue(ids.isEmpty());
+
+		// Sharing the group should make the message sendable
+		db.setGroupVisibility(txn, contactId, groupId, true);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		assertEquals(Collections.singletonList(messageId), ids);
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertEquals(Collections.singletonList(messageId), ids);
+
+		// Unsharing the group should make the message unsendable
+		db.setGroupVisibility(txn, contactId, groupId, false);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		assertTrue(ids.isEmpty());
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertTrue(ids.isEmpty());
+
+		// Making the group invisible should make the message unsendable
+		db.removeGroupVisibility(txn, contactId, groupId);
+		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		assertTrue(ids.isEmpty());
+		ids = db.getMessagesToOffer(txn, contactId, 100);
+		assertTrue(ids.isEmpty());
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
 	@Test
 	public void testSendableMessagesMustBeShared() throws Exception {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add a contact, a group and an unshared message
+		// Add a contact, a shared group and an unshared message
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
 				true, true));
 		db.addGroup(txn, group);
-		db.addVisibility(txn, contactId, groupId);
+		db.addGroupVisibility(txn, contactId, groupId, true);
 		db.addMessage(txn, message, DELIVERED, false);
 		db.addStatus(txn, contactId, messageId, false, false);
 
@@ -287,12 +342,12 @@ public class H2DatabaseTest extends BriarTestCase {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add a contact, a group and a message
+		// Add a contact, a shared group and a shared message
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
 				true, true));
 		db.addGroup(txn, group);
-		db.addVisibility(txn, contactId, groupId);
+		db.addGroupVisibility(txn, contactId, groupId, true);
 		db.addMessage(txn, message, DELIVERED, true);
 		db.addStatus(txn, contactId, messageId, false, false);
 
@@ -314,12 +369,12 @@ public class H2DatabaseTest extends BriarTestCase {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add a contact and a group
+		// Add a contact and a visible group
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
 				true, true));
 		db.addGroup(txn, group);
-		db.addVisibility(txn, contactId, groupId);
+		db.addGroupVisibility(txn, contactId, groupId, false);
 
 		// Add some messages to ack
 		MessageId messageId1 = new MessageId(TestUtils.getRandomId());
@@ -351,12 +406,12 @@ public class H2DatabaseTest extends BriarTestCase {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add a contact, a group and a message
+		// Add a contact, a shared group and a shared message
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
 				true, true));
 		db.addGroup(txn, group);
-		db.addVisibility(txn, contactId, groupId);
+		db.addGroupVisibility(txn, contactId, groupId, true);
 		db.addMessage(txn, message, DELIVERED, true);
 		db.addStatus(txn, contactId, messageId, false, false);
 
@@ -515,12 +570,12 @@ public class H2DatabaseTest extends BriarTestCase {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add a contact and a group
+		// Add a contact and a shared group
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
 				true, true));
 		db.addGroup(txn, group);
-		db.addVisibility(txn, contactId, groupId);
+		db.addGroupVisibility(txn, contactId, groupId, true);
 
 		// The message is not in the database
 		assertFalse(db.containsVisibleMessage(txn, contactId, messageId));
@@ -569,7 +624,7 @@ public class H2DatabaseTest extends BriarTestCase {
 	}
 
 	@Test
-	public void testVisibility() throws Exception {
+	public void testGroupVisibility() throws Exception {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
@@ -580,52 +635,33 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addGroup(txn, group);
 
 		// The group should not be visible to the contact
-		assertEquals(Collections.emptyList(), db.getVisibility(txn, groupId));
+		assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId));
+		assertEquals(Collections.emptyList(),
+				db.getGroupVisibility(txn, groupId));
 
 		// Make the group visible to the contact
-		db.addVisibility(txn, contactId, groupId);
+		db.addGroupVisibility(txn, contactId, groupId, false);
+		assertEquals(VISIBLE, db.getGroupVisibility(txn, contactId, groupId));
 		assertEquals(Collections.singletonList(contactId),
-				db.getVisibility(txn, groupId));
-
-		// Make the group invisible again
-		db.removeVisibility(txn, contactId, groupId);
-		assertEquals(Collections.emptyList(), db.getVisibility(txn, groupId));
+				db.getGroupVisibility(txn, groupId));
 
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testMultipleGroupChanges() throws Exception {
-		// Create some groups
-		List<Group> groups = new ArrayList<>();
-		for (int i = 0; i < 100; i++) {
-			GroupId id = new GroupId(TestUtils.getRandomId());
-			ClientId clientId = new ClientId(TestUtils.getRandomString(5));
-			byte[] descriptor = new byte[0];
-			groups.add(new Group(id, clientId, descriptor));
-		}
+		// Share the group with the contact
+		db.setGroupVisibility(txn, contactId, groupId, true);
+		assertEquals(SHARED, db.getGroupVisibility(txn, contactId, groupId));
+		assertEquals(Collections.singletonList(contactId),
+				db.getGroupVisibility(txn, groupId));
 
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
+		// Unshare the group with the contact
+		db.setGroupVisibility(txn, contactId, groupId, false);
+		assertEquals(VISIBLE, db.getGroupVisibility(txn, contactId, groupId));
+		assertEquals(Collections.singletonList(contactId),
+				db.getGroupVisibility(txn, groupId));
 
-		// Add a contact and the groups
-		db.addLocalAuthor(txn, localAuthor);
-		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
-				true, true));
-		for (Group g : groups) db.addGroup(txn, g);
-
-		// Make the groups visible to the contact
-		Collections.shuffle(groups);
-		for (Group g : groups) db.addVisibility(txn, contactId, g.getId());
-
-		// Make some of the groups invisible to the contact and remove them all
-		Collections.shuffle(groups);
-		for (Group g : groups) {
-			if (Math.random() < 0.5)
-				db.removeVisibility(txn, contactId, g.getId());
-			db.removeGroup(txn, g.getId());
-		}
+		// Make the group invisible again
+		db.removeGroupVisibility(txn, contactId, groupId);
+		assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId));
+		assertEquals(Collections.emptyList(),
+				db.getGroupVisibility(txn, groupId));
 
 		db.commitTransaction(txn);
 		db.close();
@@ -1144,15 +1180,15 @@ public class H2DatabaseTest extends BriarTestCase {
 		metadata1.put("foo", new byte[]{'b', 'a', 'r'});
 		db.mergeMessageMetadata(txn, messageId1, metadata1);
 
-		for (int i = 1; i <= 2; i++) {
+		for (int i = 0; i < 2; i++) {
 			Metadata query;
-			if (i == 1) {
+			if (i == 0) {
 				// Query the metadata with an empty query
 				query = new Metadata();
 			} else {
 				// Query for foo
 				query = new Metadata();
-				query.put("foo", metadata.get("foo"));
+				query.put("foo", new byte[]{'b', 'a', 'r'});
 			}
 
 			db.setMessageState(txn, messageId, DELIVERED);
@@ -1160,32 +1196,39 @@ public class H2DatabaseTest extends BriarTestCase {
 			Map<MessageId, Metadata> all =
 					db.getMessageMetadata(txn, groupId, query);
 			assertEquals(2, all.size());
-			assertEquals(2, all.get(messageId).size());
-			assertEquals(1, all.get(messageId1).size());
+			assertMetadataEquals(metadata, all.get(messageId));
+			assertMetadataEquals(metadata1, all.get(messageId1));
 
 			// No metadata for unknown messages
 			db.setMessageState(txn, messageId, UNKNOWN);
-			db.setMessageState(txn, messageId1, UNKNOWN);
 			all = db.getMessageMetadata(txn, groupId, query);
-			assertTrue(all.isEmpty());
+			assertEquals(1, all.size());
+			assertMetadataEquals(metadata1, all.get(messageId1));
 
 			// No metadata for invalid messages
 			db.setMessageState(txn, messageId, INVALID);
-			db.setMessageState(txn, messageId1, INVALID);
 			all = db.getMessageMetadata(txn, groupId, query);
-			assertTrue(all.isEmpty());
+			assertEquals(1, all.size());
+			assertMetadataEquals(metadata1, all.get(messageId1));
 
 			// No metadata for pending messages
 			db.setMessageState(txn, messageId, PENDING);
-			db.setMessageState(txn, messageId1, PENDING);
 			all = db.getMessageMetadata(txn, groupId, query);
-			assertTrue(all.isEmpty());
+			assertEquals(1, all.size());
+			assertMetadataEquals(metadata1, all.get(messageId1));
 		}
 
 		db.commitTransaction(txn);
 		db.close();
 	}
 
+	private void assertMetadataEquals(Metadata m1, Metadata m2) {
+		assertEquals(m1.keySet(), m2.keySet());
+		for (Entry<String, byte[]> e : m1.entrySet()) {
+			assertArrayEquals(e.getValue(), m2.get(e.getKey()));
+		}
+	}
+
 	@Test
 	public void testMessageDependencies() throws Exception {
 		MessageId messageId1 = new MessageId(TestUtils.getRandomId());
@@ -1398,16 +1441,12 @@ public class H2DatabaseTest extends BriarTestCase {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add a contact
+		// Add a contact, a shared group and a shared message
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
 				true, true));
-
-		// Add a group and make it visible to the contact
 		db.addGroup(txn, group);
-		db.addVisibility(txn, contactId, groupId);
-
-		// Add a message to the group
+		db.addGroupVisibility(txn, contactId, groupId, true);
 		db.addMessage(txn, message, DELIVERED, true);
 		db.addStatus(txn, contactId, messageId, false, false);
 
@@ -1471,33 +1510,7 @@ public class H2DatabaseTest extends BriarTestCase {
 	}
 
 	@Test
-	public void testGroupsVisibleToContacts() throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a contact and a group
-		db.addLocalAuthor(txn, localAuthor);
-		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
-				true, true));
-		db.addGroup(txn, group);
-
-		// The group should not be visible to the contact
-		assertFalse(db.containsVisibleGroup(txn, contactId, groupId));
-
-		// Make the group visible to the contact
-		db.addVisibility(txn, contactId, groupId);
-		assertTrue(db.containsVisibleGroup(txn, contactId, groupId));
-
-		// Make the group invisible to the contact
-		db.removeVisibility(txn, contactId, groupId);
-		assertFalse(db.containsVisibleGroup(txn, contactId, groupId));
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testDifferentLocalPseudonymsCanHaveTheSameContact()
+	public void testDifferentLocalAuthorsCanHaveTheSameContact()
 			throws Exception {
 		AuthorId localAuthorId1 = new AuthorId(TestUtils.getRandomId());
 		LocalAuthor localAuthor1 = new LocalAuthor(localAuthorId1, "Carol",
@@ -1506,11 +1519,11 @@ public class H2DatabaseTest extends BriarTestCase {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add two local pseudonyms
+		// Add two local authors
 		db.addLocalAuthor(txn, localAuthor);
 		db.addLocalAuthor(txn, localAuthor1);
 
-		// Add the same contact for each local pseudonym
+		// Add the same contact for each local author
 		ContactId contactId =
 				db.addContact(txn, author, localAuthorId, true, true);
 		ContactId contactId1 =
@@ -1531,12 +1544,12 @@ public class H2DatabaseTest extends BriarTestCase {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add a contact, a group and a message
+		// Add a contact, a shared group and a shared message
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId,
 				true, true));
 		db.addGroup(txn, group);
-		db.addVisibility(txn, contactId, groupId);
+		db.addGroupVisibility(txn, contactId, groupId, true);
 		db.addMessage(txn, message, DELIVERED, true);
 		db.addStatus(txn, contactId, messageId, false, false);