diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml
index 2feaa4e95fbea9bbb14af009cc80761f3beb8141..a7a72e1fbf12397bf9c5d07f34013c1654134e9e 100644
--- a/.idea/codeStyleSettings.xml
+++ b/.idea/codeStyleSettings.xml
@@ -31,6 +31,8 @@
           </value>
         </option>
         <option name="RIGHT_MARGIN" value="100" />
+        <option name="JD_ALIGN_PARAM_COMMENTS" value="false" />
+        <option name="JD_ALIGN_EXCEPTION_COMMENTS" value="false" />
         <AndroidXmlCodeStyleSettings>
           <option name="USE_CUSTOM_SETTINGS" value="true" />
         </AndroidXmlCodeStyleSettings>
diff --git a/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationManager.java b/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationManager.java
index 489e51a5917bc5f477cef44bd6ff2d71d82d9218..59475c9749bcf73768ae2cfef84a164ce0b67d1e 100644
--- a/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationManager.java
+++ b/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationManager.java
@@ -1,5 +1,6 @@
 package org.briarproject.api.privategroup.invitation;
 
+import org.briarproject.api.clients.ProtocolStateException;
 import org.briarproject.api.clients.SessionId;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
@@ -26,22 +27,41 @@ public interface GroupInvitationManager {
 	/**
 	 * Sends an invitation to share the given private group with the given
 	 * contact, including an optional message.
+	 *
+	 * @throws ProtocolStateException if the group is no longer eligible to be
+	 * shared with the contact, for example because an invitation is already
+	 * pending.
 	 */
 	void sendInvitation(GroupId g, ContactId c, @Nullable String message,
 			long timestamp, byte[] signature) throws DbException;
 
 	/**
 	 * Responds to a pending private group invitation from the given contact.
+	 *
+	 * @throws ProtocolStateException if the invitation is no longer pending,
+	 * for example because the group has been dissolved.
 	 */
 	void respondToInvitation(ContactId c, PrivateGroup g, boolean accept)
 			throws DbException;
 
 	/**
 	 * Responds to a pending private group invitation from the given contact.
+	 *
+	 * @throws ProtocolStateException if the invitation is no longer pending,
+	 * for example because the group has been dissolved.
 	 */
 	void respondToInvitation(ContactId c, SessionId s, boolean accept)
 			throws DbException;
 
+	/**
+	 * Makes the user's relationship with the given contact visible to the
+	 * given private group.
+	 *
+	 * @throws ProtocolStateException if the relationship is no longer eligible
+	 * to be revealed, for example because the contact has revealed it.
+	 */
+	void revealRelationship(ContactId c, GroupId g) throws DbException;
+
 	/**
 	 * Returns all private group invitation messages related to the given
 	 * contact.
diff --git a/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java b/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java
index c2bdf6cf493060e57de864e01fbb16c985c5150a..5ff1193f93ed4f4534f32d99a78a77b4908aa2f4 100644
--- a/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java
+++ b/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java
@@ -443,21 +443,26 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
 		BdfDictionary meta = clientHelper.getGroupMetadataAsDictionary(txn, g);
 		BdfList members = meta.getList(GROUP_KEY_MEMBERS);
 		Visibility v = INVISIBLE;
-		boolean foundMember = false;
+		boolean foundMember = false, changed = false;
 		for (int i = 0 ; i < members.size(); i++) {
 			BdfDictionary d = members.getDictionary(i);
 			AuthorId memberId = new AuthorId(d.getRaw(KEY_MEMBER_ID));
 			if (a.equals(memberId)) {
 				foundMember = true;
-				Visibility vOld = getVisibility(d);
-				if (vOld != INVISIBLE) throw new ProtocolStateException();
-				v = byContact ? REVEALED_BY_CONTACT : REVEALED_BY_US;
-				d.put(GROUP_KEY_VISIBILITY, v.getInt());
+				// Don't update the visibility if the contact is already visible
+				if (getVisibility(d) == INVISIBLE) {
+					changed = true;
+					v = byContact ? REVEALED_BY_CONTACT : REVEALED_BY_US;
+					d.put(GROUP_KEY_VISIBILITY, v.getInt());
+				}
+				break;
 			}
 		}
 		if (!foundMember) throw new ProtocolStateException();
-		clientHelper.mergeGroupMetadata(txn, g, meta);
-		txn.attach(new ContactRelationshipRevealedEvent(g, v));
+		if (changed) {
+			clientHelper.mergeGroupMetadata(txn, g, meta);
+			txn.attach(new ContactRelationshipRevealedEvent(g, v));
+		}
 	}
 
 	@Override
diff --git a/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationManagerImpl.java b/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationManagerImpl.java
index ae11b48b850f13154046c447614061e910e8dd4c..d4a51798f1a77e512aee6355681d660eaf722e1c 100644
--- a/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationManagerImpl.java
+++ b/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationManagerImpl.java
@@ -315,6 +315,30 @@ class GroupInvitationManagerImpl extends ConversationClientImpl
 		}
 	}
 
+	@Override
+	public void revealRelationship(ContactId c, GroupId g) throws DbException {
+		Transaction txn = db.startTransaction(false);
+		try {
+			// Look up the session
+			Contact contact = db.getContact(txn, c);
+			GroupId contactGroupId = getContactGroup(contact).getId();
+			StoredSession ss = getSession(txn, contactGroupId, getSessionId(g));
+			if (ss == null) throw new IllegalArgumentException();
+			// Parse the session
+			PeerSession session = sessionParser
+					.parsePeerSession(contactGroupId, ss.bdfSession);
+			// Handle the join action
+			session = peerEngine.onJoinAction(txn, session);
+			// Store the updated session
+			storeSession(txn, ss.storageId, session);
+			db.commitTransaction(txn);
+		} catch (FormatException e) {
+			throw new DbException(e);
+		} finally {
+			db.endTransaction(txn);
+		}
+	}
+
 	private <S extends Session> S handleAction(Transaction txn,
 			LocalAction type, S session, ProtocolEngine<S> engine)
 			throws DbException, FormatException {
@@ -435,9 +459,6 @@ class GroupInvitationManagerImpl extends ConversationClientImpl
 			db.commitTransaction(txn);
 			// If there's no session, the contact can be invited
 			if (ss == null) return true;
-			// If there's a session, it should be a creator session
-			if (sessionParser.getRole(ss.bdfSession) != CREATOR)
-				throw new IllegalArgumentException();
 			// If the session's in the start state, the contact can be invited
 			CreatorSession session = sessionParser
 					.parseCreatorSession(contactGroupId, ss.bdfSession);
diff --git a/briar-core/src/org/briarproject/privategroup/invitation/PeerProtocolEngine.java b/briar-core/src/org/briarproject/privategroup/invitation/PeerProtocolEngine.java
index 6d3c721f38fea70a2a25c5396abc0e0f54bce442..84aa82ced367d97dd8f063eeed21aaa34a0b37dd 100644
--- a/briar-core/src/org/briarproject/privategroup/invitation/PeerProtocolEngine.java
+++ b/briar-core/src/org/briarproject/privategroup/invitation/PeerProtocolEngine.java
@@ -3,6 +3,8 @@ package org.briarproject.privategroup.invitation;
 import org.briarproject.api.FormatException;
 import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.clients.ProtocolStateException;
+import org.briarproject.api.contact.Contact;
+import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.Transaction;
@@ -179,6 +181,7 @@ class PeerProtocolEngine extends AbstractProtocolEngine<PeerSession> {
 		} catch (FormatException e) {
 			throw new DbException(e); // Invalid group metadata
 		}
+		// The relationship is already marked visible to the group
 		// Move to the BOTH_JOINED state
 		return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
 				sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
@@ -228,6 +231,12 @@ class PeerProtocolEngine extends AbstractProtocolEngine<PeerSession> {
 		} catch (FormatException e) {
 			throw new DbException(e); // Invalid group metadata
 		}
+		try {
+			// Mark the relationship visible to the group, revealed by contact
+			relationshipRevealed(txn, s, true);
+		} catch (FormatException e) {
+			throw new DbException(e); // Invalid group metadata
+		}
 		// Move to the BOTH_JOINED state
 		return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
 				sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
@@ -254,6 +263,8 @@ class PeerProtocolEngine extends AbstractProtocolEngine<PeerSession> {
 		Message sent = sendJoinMessage(txn, s, false);
 		// Start syncing the private group with the contact
 		syncPrivateGroupWithContact(txn, s, true);
+		// Mark the relationship visible to the group, revealed by contact
+		relationshipRevealed(txn, s, true);
 		// Move to the BOTH_JOINED state
 		return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
 				sent.getId(), m.getId(), sent.getTimestamp(), BOTH_JOINED);
@@ -266,6 +277,8 @@ class PeerProtocolEngine extends AbstractProtocolEngine<PeerSession> {
 			return abort(txn, s);
 		// Start syncing the private group with the contact
 		syncPrivateGroupWithContact(txn, s, true);
+		// Mark the relationship visible to the group, revealed by us
+		relationshipRevealed(txn, s, false);
 		// Move to the BOTH_JOINED state
 		return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
 				s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(),
@@ -321,4 +334,12 @@ class PeerProtocolEngine extends AbstractProtocolEngine<PeerSession> {
 				sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
 				ERROR);
 	}
+
+	private void relationshipRevealed(Transaction txn, PeerSession s,
+			boolean byContact) throws DbException, FormatException {
+		ContactId contactId = getContactId(txn, s.getContactGroupId());
+		Contact contact = db.getContact(txn, contactId);
+		privateGroupManager.relationshipRevealed(txn, s.getPrivateGroupId(),
+				contact.getAuthor().getId(), byContact);
+	}
 }