diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java b/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java
index 71b351452eeff6224f68a912b2a0211dbd2f28fe..ea695a5fb9ce682aa66cf47f2ce2282519c3ef87 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/Database.java
@@ -96,9 +96,12 @@ interface Database<T> {
 
 	/**
 	 * Stores a message.
+	 *
+	 * @param sender the contact from whom the message was received, or null
+	 * if the message was created locally.
 	 */
-	void addMessage(T txn, Message m, State state, boolean shared)
-			throws DbException;
+	void addMessage(T txn, Message m, State state, boolean shared,
+			@Nullable ContactId sender) throws DbException;
 
 	/**
 	 * Adds a dependency between two messages in the given group.
@@ -111,16 +114,6 @@ interface Database<T> {
 	 */
 	void addOfferedMessage(T txn, ContactId c, MessageId m) throws DbException;
 
-	/**
-	 * Initialises the status of the given message with respect to the given
-	 * contact.
-	 *
-	 * @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)
-			throws DbException;
-
 	/**
 	 * Stores a transport.
 	 */
@@ -279,7 +272,7 @@ interface Database<T> {
 	 * <p/>
 	 * Read-only.
 	 */
-	Collection<ContactId> getGroupVisibility(T txn, GroupId g)
+	Map<ContactId, Boolean> getGroupVisibility(T txn, GroupId g)
 			throws DbException;
 
 	/**
@@ -573,13 +566,6 @@ interface Database<T> {
 	 */
 	void removeMessage(T txn, MessageId m) throws DbException;
 
-	/**
-	 * Removes an offered message that was offered by the given contact, or
-	 * returns false if there is no such message.
-	 */
-	boolean removeOfferedMessage(T txn, ContactId c, MessageId m)
-			throws DbException;
-
 	/**
 	 * Removes the given offered messages that were offered by the given
 	 * contact.
@@ -587,12 +573,6 @@ interface Database<T> {
 	void removeOfferedMessages(T txn, ContactId c,
 			Collection<MessageId> requested) throws DbException;
 
-	/**
-	 * Removes the status of the given message with respect to the given
-	 * contact.
-	 */
-	void removeStatus(T txn, ContactId c, MessageId m) throws DbException;
-
 	/**
 	 * Removes a transport (and all associated state) from the database.
 	 */
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java
index 570ec013a9750564fcf40ea1ed687719ab8a807a..53b379073d5d4c36cec8f50d93e526f596c5f334 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java
@@ -213,7 +213,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		if (!db.containsGroup(txn, m.getGroupId()))
 			throw new NoSuchGroupException();
 		if (!db.containsMessage(txn, m.getId())) {
-			addMessage(txn, m, DELIVERED, shared, null);
+			db.addMessage(txn, m, DELIVERED, shared, null);
 			transaction.attach(new MessageAddedEvent(m, null));
 			transaction.attach(new MessageStateChangedEvent(m.getId(), true,
 					DELIVERED));
@@ -222,16 +222,6 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		db.mergeMessageMetadata(txn, m.getId(), meta);
 	}
 
-	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.getGroupVisibility(txn, m.getGroupId())) {
-			boolean offered = db.removeOfferedMessage(txn, c, m.getId());
-			boolean seen = offered || (sender != null && c.equals(sender));
-			db.addStatus(txn, c, m.getId(), seen, seen);
-		}
-	}
-
 	@Override
 	public void addTransport(Transaction transaction, TransportId t,
 			int maxLatency) throws DbException {
@@ -673,7 +663,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 				db.raiseSeenFlag(txn, c, m.getId());
 				db.raiseAckFlag(txn, c, m.getId());
 			} else {
-				addMessage(txn, m, UNKNOWN, false, c);
+				db.addMessage(txn, m, UNKNOWN, false, c);
 				transaction.attach(new MessageAddedEvent(m, c));
 			}
 			transaction.attach(new MessageToAckEvent(c));
@@ -741,7 +731,8 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		GroupId id = g.getId();
 		if (!db.containsGroup(txn, id))
 			throw new NoSuchGroupException();
-		Collection<ContactId> affected = db.getGroupVisibility(txn, id);
+		Collection<ContactId> affected =
+				db.getGroupVisibility(txn, id).keySet();
 		db.removeGroup(txn, id);
 		transaction.attach(new GroupRemovedEvent(g));
 		transaction.attach(new GroupVisibilityUpdatedEvent(affected));
@@ -811,19 +802,9 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 			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);
-		}
+		if (old == INVISIBLE) db.addGroupVisibility(txn, c, g, v == SHARED);
+		else if (v == INVISIBLE) db.removeGroupVisibility(txn, c, g);
+		else db.setGroupVisibility(txn, c, g, v == SHARED);
 		List<ContactId> affected = Collections.singletonList(c);
 		transaction.attach(new GroupVisibilityUpdatedEvent(affected));
 	}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java b/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java
index b63dd9e62060a02703344a2d49e6f8f726033b5c..dcd758f9e3208a89044db35427ebfba5bb9dbf8d 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java
@@ -71,7 +71,7 @@ import static org.briarproject.bramble.db.ExponentialBackoff.calculateExpiry;
 abstract class JdbcDatabase implements Database<Connection> {
 
 	// Package access for testing
-	static final int CODE_SCHEMA_VERSION = 34;
+	static final int CODE_SCHEMA_VERSION = 35;
 
 	private static final String CREATE_SETTINGS =
 			"CREATE TABLE settings"
@@ -189,6 +189,13 @@ abstract class JdbcDatabase implements Database<Connection> {
 			"CREATE TABLE statuses"
 					+ " (messageId _HASH NOT NULL,"
 					+ " contactId INT NOT NULL,"
+					+ " groupId _HASH NOT NULL," // Denormalised
+					+ " timestamp BIGINT NOT NULL," // Denormalised
+					+ " length INT NOT NULL," // Denormalised
+					+ " state INT NOT NULL," // Denormalised
+					+ " groupShared BOOLEAN NOT NULL," // Denormalised
+					+ " messageShared BOOLEAN NOT NULL," // Denormalised
+					+ " deleted BOOLEAN NOT NULL," // Denormalised
 					+ " ack BOOLEAN NOT NULL,"
 					+ " seen BOOLEAN NOT NULL,"
 					+ " requested BOOLEAN NOT NULL,"
@@ -200,6 +207,9 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " ON DELETE CASCADE,"
 					+ " FOREIGN KEY (contactId)"
 					+ " REFERENCES contacts (contactId)"
+					+ " ON DELETE CASCADE,"
+					+ " FOREIGN KEY (groupId)"
+					+ " REFERENCES groups (groupId)"
 					+ " ON DELETE CASCADE)";
 
 	private static final String CREATE_TRANSPORTS =
@@ -253,6 +263,14 @@ abstract class JdbcDatabase implements Database<Connection> {
 			"CREATE INDEX IF NOT EXISTS messageMetadataByGroupIdState"
 					+ " ON messageMetadata (groupId, state)";
 
+	private static final String INDEX_STATUSES_BY_CONTACT_ID_GROUP_ID =
+			"CREATE INDEX IF NOT EXISTS statusesByContactIdGroupId"
+					+ " ON statuses (contactId, groupId)";
+
+	private static final String INDEX_STATUSES_BY_CONTACT_ID_TIMESTAMP =
+			"CREATE INDEX IF NOT EXISTS statusesByContactIdTimestamp"
+					+ " ON statuses (contactId, timestamp)";
+
 	private static final Logger LOG =
 			Logger.getLogger(JdbcDatabase.class.getName());
 
@@ -401,6 +419,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 			s.executeUpdate(INDEX_CONTACTS_BY_AUTHOR_ID);
 			s.executeUpdate(INDEX_GROUPS_BY_CLIENT_ID);
 			s.executeUpdate(INDEX_MESSAGE_METADATA_BY_GROUP_ID_STATE);
+			s.executeUpdate(INDEX_STATUSES_BY_CONTACT_ID_GROUP_ID);
+			s.executeUpdate(INDEX_STATUSES_BY_CONTACT_ID_TIMESTAMP);
 			s.close();
 		} catch (SQLException e) {
 			tryToClose(s);
@@ -581,7 +601,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 
 	@Override
 	public void addGroupVisibility(Connection txn, ContactId c, GroupId g,
-			boolean shared) throws DbException {
+			boolean groupShared) throws DbException {
 		PreparedStatement ps = null;
 		try {
 			String sql = "INSERT INTO groupVisibilities"
@@ -590,16 +610,50 @@ abstract class JdbcDatabase implements Database<Connection> {
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			ps.setBytes(2, g.getBytes());
-			ps.setBoolean(3, shared);
+			ps.setBoolean(3, groupShared);
 			int affected = ps.executeUpdate();
 			if (affected != 1) throw new DbStateException();
 			ps.close();
+			// Create a status row for each message in the group
+			addStatus(txn, c, g, groupShared);
 		} catch (SQLException e) {
 			tryToClose(ps);
 			throw new DbException(e);
 		}
 	}
 
+	private void addStatus(Connection txn, ContactId c, GroupId g,
+			boolean groupShared) throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			String sql = "SELECT messageId, timestamp, state, shared,"
+					+ " length, raw IS NULL"
+					+ " FROM messages"
+					+ " WHERE groupId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setBytes(1, g.getBytes());
+			rs = ps.executeQuery();
+			while (rs.next()) {
+				MessageId id = new MessageId(rs.getBytes(1));
+				long timestamp = rs.getLong(2);
+				State state = State.fromValue(rs.getInt(3));
+				boolean messageShared = rs.getBoolean(4);
+				int length = rs.getInt(5);
+				boolean deleted = rs.getBoolean(6);
+				boolean seen = removeOfferedMessage(txn, c, id);
+				addStatus(txn, id, c, g, timestamp, length, state, groupShared,
+						messageShared, deleted, seen);
+			}
+			rs.close();
+			ps.close();
+		} catch (SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
 	@Override
 	public void addLocalAuthor(Connection txn, LocalAuthor a)
 			throws DbException {
@@ -627,7 +681,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 
 	@Override
 	public void addMessage(Connection txn, Message m, State state,
-			boolean shared) throws DbException {
+			boolean messageShared, @Nullable ContactId sender)
+			throws DbException {
 		PreparedStatement ps = null;
 		try {
 			String sql = "INSERT INTO messages (messageId, groupId, timestamp,"
@@ -638,13 +693,24 @@ abstract class JdbcDatabase implements Database<Connection> {
 			ps.setBytes(2, m.getGroupId().getBytes());
 			ps.setLong(3, m.getTimestamp());
 			ps.setInt(4, state.getValue());
-			ps.setBoolean(5, shared);
+			ps.setBoolean(5, messageShared);
 			byte[] raw = m.getRaw();
 			ps.setInt(6, raw.length);
 			ps.setBytes(7, raw);
 			int affected = ps.executeUpdate();
 			if (affected != 1) throw new DbStateException();
 			ps.close();
+			// Create a status row for each contact that can see the group
+			Map<ContactId, Boolean> visibility =
+					getGroupVisibility(txn, m.getGroupId());
+			for (Entry<ContactId, Boolean> e : visibility.entrySet()) {
+				ContactId c = e.getKey();
+				boolean offered = removeOfferedMessage(txn, c, m.getId());
+				boolean seen = offered || (sender != null && c.equals(sender));
+				addStatus(txn, m.getId(), c, m.getGroupId(), m.getTimestamp(),
+						m.getLength(), state, e.getValue(), messageShared,
+						false, seen);
+			}
 		} catch (SQLException e) {
 			tryToClose(ps);
 			throw new DbException(e);
@@ -682,19 +748,28 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	@Override
-	public void addStatus(Connection txn, ContactId c, MessageId m, boolean ack,
-			boolean seen) throws DbException {
+	private void addStatus(Connection txn, MessageId m, ContactId c, GroupId g,
+			long timestamp, int length, State state, boolean groupShared,
+			boolean messageShared, boolean deleted, boolean seen)
+			throws DbException {
 		PreparedStatement ps = null;
 		try {
-			String sql = "INSERT INTO statuses (messageId, contactId, ack,"
-					+ " seen, requested, expiry, txCount)"
-					+ " VALUES (?, ?, ?, ?, FALSE, 0, 0)";
+			String sql = "INSERT INTO statuses (messageId, contactId, groupId,"
+					+ " timestamp, length, state, groupShared, messageShared,"
+					+ " deleted, ack, seen, requested, expiry, txCount)"
+					+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, FALSE, 0, 0)";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, m.getBytes());
 			ps.setInt(2, c.getInt());
-			ps.setBoolean(3, ack);
-			ps.setBoolean(4, seen);
+			ps.setBytes(3, g.getBytes());
+			ps.setLong(4, timestamp);
+			ps.setInt(5, length);
+			ps.setInt(6, state.getValue());
+			ps.setBoolean(7, groupShared);
+			ps.setBoolean(8, messageShared);
+			ps.setBoolean(9, deleted);
+			ps.setBoolean(10, seen);
+			ps.setBoolean(11, seen);
 			int affected = ps.executeUpdate();
 			if (affected != 1) throw new DbStateException();
 			ps.close();
@@ -946,12 +1021,9 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql = "SELECT NULL FROM messages AS m"
-					+ " JOIN groupVisibilities AS gv"
-					+ " ON m.groupId = gv.groupId"
-					+ " WHERE messageId = ?"
-					+ " AND contactId = ?"
-					+ " AND m.shared = TRUE";
+			String sql = "SELECT NULL FROM statuses"
+					+ " WHERE messageId = ? AND contactId = ?"
+					+ " AND messageShared = TRUE";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, m.getBytes());
 			ps.setInt(2, c.getInt());
@@ -1003,6 +1075,13 @@ abstract class JdbcDatabase implements Database<Connection> {
 			if (affected < 0) throw new DbStateException();
 			if (affected > 1) throw new DbStateException();
 			ps.close();
+			// Update denormalised column in statuses
+			sql = "UPDATE statuses SET deleted = TRUE WHERE messageId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setBytes(1, m.getBytes());
+			affected = ps.executeUpdate();
+			if (affected < 0) throw new DbStateException();
+			ps.close();
 		} catch (SQLException e) {
 			tryToClose(ps);
 			throw new DbException(e);
@@ -1231,18 +1310,19 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	@Override
-	public Collection<ContactId> getGroupVisibility(Connection txn, GroupId g)
+	public Map<ContactId, Boolean> getGroupVisibility(Connection txn, GroupId g)
 			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql = "SELECT contactId FROM groupVisibilities"
+			String sql = "SELECT contactId, shared FROM groupVisibilities"
 					+ " WHERE groupId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, g.getBytes());
 			rs = ps.executeQuery();
-			List<ContactId> visible = new ArrayList<>();
-			while (rs.next()) visible.add(new ContactId(rs.getInt(1)));
+			Map<ContactId, Boolean> visible = new HashMap<>();
+			while (rs.next())
+				visible.put(new ContactId(rs.getInt(1)), rs.getBoolean(2));
 			rs.close();
 			ps.close();
 			return visible;
@@ -1524,12 +1604,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql = "SELECT m.messageId, txCount > 0, seen"
-					+ " FROM messages AS m"
-					+ " JOIN statuses AS s"
-					+ " ON m.messageId = s.messageId"
-					+ " WHERE groupId = ?"
-					+ " AND contactId = ?";
+			String sql = "SELECT messageId, txCount > 0, seen FROM statuses"
+					+ " WHERE groupId = ? AND contactId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, g.getBytes());
 			ps.setInt(2, c.getInt());
@@ -1552,15 +1628,13 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	@Override
-	public MessageStatus getMessageStatus(Connection txn,
-			ContactId c, MessageId m) throws DbException {
+	public MessageStatus getMessageStatus(Connection txn, ContactId c,
+			MessageId m) throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql = "SELECT txCount > 0, seen"
-					+ " FROM statuses"
-					+ " WHERE messageId = ?"
-					+ " AND contactId = ?";
+			String sql = "SELECT txCount > 0, seen FROM statuses"
+					+ " WHERE messageId = ? AND contactId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, m.getBytes());
 			ps.setInt(2, c.getInt());
@@ -1702,14 +1776,10 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		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"
-					+ " AND gv.contactId = s.contactId"
-					+ " WHERE gv.contactId = ? AND gv.shared = TRUE"
-					+ " AND state = ? AND m.shared = TRUE AND raw IS NOT NULL"
+			String sql = "SELECT messageId FROM statuses"
+					+ " WHERE contactId = ? AND state = ?"
+					+ " AND groupShared = TRUE AND messageShared = TRUE"
+					+ " AND deleted = FALSE"
 					+ " AND seen = FALSE AND requested = FALSE"
 					+ " AND expiry < ?"
 					+ " ORDER BY timestamp LIMIT ?";
@@ -1763,14 +1833,10 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		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"
-					+ " AND gv.contactId = s.contactId"
-					+ " WHERE gv.contactId = ? AND gv.shared = TRUE"
-					+ " AND state = ? AND m.shared = TRUE AND raw IS NOT NULL"
+			String sql = "SELECT length, messageId FROM statuses"
+					+ " WHERE contactId = ? AND state = ?"
+					+ " AND groupShared = TRUE AND messageShared = TRUE"
+					+ " AND deleted = FALSE"
 					+ " AND seen = FALSE"
 					+ " AND expiry < ?"
 					+ " ORDER BY timestamp";
@@ -1834,8 +1900,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	@Override
-	public Collection<MessageId> getMessagesToShare(
-			Connection txn, ClientId c) throws DbException {
+	public Collection<MessageId> getMessagesToShare(Connection txn, ClientId c)
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
@@ -1894,14 +1960,10 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		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"
-					+ " AND gv.contactId = s.contactId"
-					+ " WHERE gv.contactId = ? AND gv.shared = TRUE"
-					+ " AND state = ? AND m.shared = TRUE AND raw IS NOT NULL"
+			String sql = "SELECT length, messageId FROM statuses"
+					+ " WHERE contactId = ? AND state = ?"
+					+ " AND groupShared = TRUE AND messageShared = TRUE"
+					+ " AND deleted = FALSE"
 					+ " AND seen = FALSE AND requested = TRUE"
 					+ " AND expiry < ?"
 					+ " ORDER BY timestamp";
@@ -2380,6 +2442,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 			int affected = ps.executeUpdate();
 			if (affected != 1) throw new DbStateException();
 			ps.close();
+			// Remove status rows for the messages in the group
+			for (MessageId m : getMessageIds(txn, g)) removeStatus(txn, c, m);
 		} catch (SQLException e) {
 			tryToClose(ps);
 			throw new DbException(e);
@@ -2419,8 +2483,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	@Override
-	public boolean removeOfferedMessage(Connection txn, ContactId c,
+	private boolean removeOfferedMessage(Connection txn, ContactId c,
 			MessageId m) throws DbException {
 		PreparedStatement ps = null;
 		try {
@@ -2464,16 +2527,15 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	@Override
-	public void removeStatus(Connection txn, ContactId c, MessageId m)
+	private void removeStatus(Connection txn, ContactId c, MessageId m)
 			throws DbException {
 		PreparedStatement ps = null;
 		try {
 			String sql = "DELETE FROM statuses"
-					+ " WHERE contactId = ? AND messageId = ?";
+					+ " WHERE messageId = ? AND contactId = ?";
 			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			ps.setBytes(2, m.getBytes());
+			ps.setBytes(1, m.getBytes());
+			ps.setInt(2, c.getInt());
 			int affected = ps.executeUpdate();
 			if (affected != 1) throw new DbStateException();
 			ps.close();
@@ -2569,6 +2631,16 @@ abstract class JdbcDatabase implements Database<Connection> {
 			int affected = ps.executeUpdate();
 			if (affected < 0 || affected > 1) throw new DbStateException();
 			ps.close();
+			// Update denormalised column in statuses
+			sql = "UPDATE statuses SET groupShared = ?"
+					+ " WHERE contactId = ? AND groupId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setBoolean(1, shared);
+			ps.setInt(2, c.getInt());
+			ps.setBytes(3, g.getBytes());
+			affected = ps.executeUpdate();
+			if (affected < 0) throw new DbStateException();
+			ps.close();
 		} catch (SQLException e) {
 			tryToClose(ps);
 			throw new DbException(e);
@@ -2587,6 +2659,14 @@ abstract class JdbcDatabase implements Database<Connection> {
 			int affected = ps.executeUpdate();
 			if (affected < 0 || affected > 1) throw new DbStateException();
 			ps.close();
+			// Update denormalised column in statuses
+			sql = "UPDATE statuses SET messageShared = TRUE"
+					+ " WHERE messageId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setBytes(1, m.getBytes());
+			affected = ps.executeUpdate();
+			if (affected < 0) throw new DbStateException();
+			ps.close();
 		} catch (SQLException e) {
 			tryToClose(ps);
 			throw new DbException(e);
@@ -2613,6 +2693,14 @@ abstract class JdbcDatabase implements Database<Connection> {
 			affected = ps.executeUpdate();
 			if (affected < 0) throw new DbStateException();
 			ps.close();
+			// Update denormalised column in statuses
+			sql = "UPDATE statuses SET state = ? WHERE messageId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setInt(1, state.getValue());
+			ps.setBytes(2, m.getBytes());
+			affected = ps.executeUpdate();
+			if (affected < 0) throw new DbStateException();
+			ps.close();
 		} catch (SQLException e) {
 			tryToClose(ps);
 			throw new DbException(e);
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java
index 03905099f1ba87970652402d0a17f9e33a3e8054..b2e4842db87c3f0a6633f1196756be456b7671b9 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabaseComponentImplTest.java
@@ -47,6 +47,7 @@ import org.briarproject.bramble.api.transport.IncomingKeys;
 import org.briarproject.bramble.api.transport.OutgoingKeys;
 import org.briarproject.bramble.api.transport.TransportKeys;
 import org.briarproject.bramble.test.BrambleMockTestCase;
+import org.briarproject.bramble.test.CaptureArgumentAction;
 import org.briarproject.bramble.test.TestUtils;
 import org.jmock.Expectations;
 import org.junit.Test;
@@ -54,9 +55,12 @@ import org.junit.Test;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
 
+import static java.util.Collections.emptyMap;
+import static java.util.Collections.singletonList;
+import static java.util.Collections.singletonMap;
 import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
 import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
 import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
@@ -158,7 +162,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 					ContactStatusChangedEvent.class)));
 			// getContacts()
 			oneOf(database).getContacts(txn);
-			will(returnValue(Collections.singletonList(contact)));
+			will(returnValue(singletonList(contact)));
 			// addGroup()
 			oneOf(database).containsGroup(txn, groupId);
 			will(returnValue(false));
@@ -169,12 +173,12 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 			will(returnValue(true));
 			// getGroups()
 			oneOf(database).getGroups(txn, clientId);
-			will(returnValue(Collections.singletonList(group)));
+			will(returnValue(singletonList(group)));
 			// removeGroup()
 			oneOf(database).containsGroup(txn, groupId);
 			will(returnValue(true));
 			oneOf(database).getGroupVisibility(txn, groupId);
-			will(returnValue(Collections.emptyList()));
+			will(returnValue(emptyMap()));
 			oneOf(database).removeGroup(txn, groupId);
 			oneOf(eventBus).broadcast(with(any(GroupRemovedEvent.class)));
 			oneOf(eventBus).broadcast(with(any(
@@ -203,11 +207,11 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 			db.addLocalAuthor(transaction, localAuthor);
 			assertEquals(contactId, db.addContact(transaction, author,
 					localAuthor.getId(), true, true));
-			assertEquals(Collections.singletonList(contact),
+			assertEquals(singletonList(contact),
 					db.getContacts(transaction));
 			db.addGroup(transaction, group); // First time - listeners called
 			db.addGroup(transaction, group); // Second time - not called
-			assertEquals(Collections.singletonList(group),
+			assertEquals(singletonList(group),
 					db.getGroups(transaction, clientId));
 			db.removeGroup(transaction, group);
 			db.removeContact(transaction, contactId);
@@ -252,13 +256,8 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 			will(returnValue(true));
 			oneOf(database).containsMessage(txn, messageId);
 			will(returnValue(false));
-			oneOf(database).addMessage(txn, message, DELIVERED, true);
+			oneOf(database).addMessage(txn, message, DELIVERED, true, null);
 			oneOf(database).mergeMessageMetadata(txn, messageId, metadata);
-			oneOf(database).getGroupVisibility(txn, groupId);
-			will(returnValue(Collections.singletonList(contactId)));
-			oneOf(database).removeOfferedMessage(txn, contactId, messageId);
-			will(returnValue(false));
-			oneOf(database).addStatus(txn, contactId, messageId, false, false);
 			oneOf(database).commitTransaction(txn);
 			// The message was added, so the listeners should be called
 			oneOf(eventBus).broadcast(with(any(MessageAddedEvent.class)));
@@ -394,7 +393,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 
 		transaction = db.startTransaction(false);
 		try {
-			Ack a = new Ack(Collections.singletonList(messageId));
+			Ack a = new Ack(singletonList(messageId));
 			db.receiveAck(transaction, contactId, a);
 			fail();
 		} catch (NoSuchContactException expected) {
@@ -415,7 +414,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 
 		transaction = db.startTransaction(false);
 		try {
-			Offer o = new Offer(Collections.singletonList(messageId));
+			Offer o = new Offer(singletonList(messageId));
 			db.receiveOffer(transaction, contactId, o);
 			fail();
 		} catch (NoSuchContactException expected) {
@@ -426,7 +425,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 
 		transaction = db.startTransaction(false);
 		try {
-			Request r = new Request(Collections.singletonList(messageId));
+			Request r = new Request(singletonList(messageId));
 			db.receiveRequest(transaction, contactId, r);
 			fail();
 		} catch (NoSuchContactException expected) {
@@ -1021,7 +1020,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 
 		Transaction transaction = db.startTransaction(false);
 		try {
-			Ack a = new Ack(Collections.singletonList(messageId));
+			Ack a = new Ack(singletonList(messageId));
 			db.receiveAck(transaction, contactId, a);
 			db.commitTransaction(transaction);
 		} finally {
@@ -1041,12 +1040,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 			will(returnValue(VISIBLE));
 			oneOf(database).containsMessage(txn, messageId);
 			will(returnValue(false));
-			oneOf(database).addMessage(txn, message, UNKNOWN, false);
-			oneOf(database).getGroupVisibility(txn, groupId);
-			will(returnValue(Collections.singletonList(contactId)));
-			oneOf(database).removeOfferedMessage(txn, contactId, messageId);
-			will(returnValue(false));
-			oneOf(database).addStatus(txn, contactId, messageId, true, true);
+			oneOf(database).addMessage(txn, message, UNKNOWN, false, contactId);
 			// Second time
 			oneOf(database).containsContact(txn, contactId);
 			will(returnValue(true));
@@ -1196,7 +1190,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 
 		Transaction transaction = db.startTransaction(false);
 		try {
-			Request r = new Request(Collections.singletonList(messageId));
+			Request r = new Request(singletonList(messageId));
 			db.receiveRequest(transaction, contactId, r);
 			db.commitTransaction(transaction);
 		} finally {
@@ -1205,7 +1199,11 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 	}
 
 	@Test
-	public void testChangingVisibilityCallsListeners() throws Exception {
+	public void testChangingVisibilityFromInvisibleToVisibleCallsListeners()
+			throws Exception {
+		AtomicReference<GroupVisibilityUpdatedEvent> event =
+				new AtomicReference<>();
+
 		context.checking(new Expectations() {{
 			oneOf(database).startTransaction();
 			will(returnValue(txn));
@@ -1214,16 +1212,13 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 			oneOf(database).containsGroup(txn, groupId);
 			will(returnValue(true));
 			oneOf(database).getGroupVisibility(txn, contactId, groupId);
-			will(returnValue(INVISIBLE)); // Not yet visible
+			will(returnValue(INVISIBLE));
 			oneOf(database).addGroupVisibility(txn, contactId, groupId, false);
-			oneOf(database).getMessageIds(txn, groupId);
-			will(returnValue(Collections.singletonList(messageId)));
-			oneOf(database).removeOfferedMessage(txn, contactId, messageId);
-			will(returnValue(false));
-			oneOf(database).addStatus(txn, contactId, messageId, false, false);
 			oneOf(database).commitTransaction(txn);
 			oneOf(eventBus).broadcast(with(any(
 					GroupVisibilityUpdatedEvent.class)));
+			will(new CaptureArgumentAction<>(event,
+					GroupVisibilityUpdatedEvent.class, 0));
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
@@ -1235,6 +1230,48 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 		} finally {
 			db.endTransaction(transaction);
 		}
+
+		GroupVisibilityUpdatedEvent e = event.get();
+		assertNotNull(e);
+		assertEquals(singletonList(contactId), e.getAffectedContacts());
+	}
+
+	@Test
+	public void testChangingVisibilityFromVisibleToInvisibleCallsListeners()
+			throws Exception {
+		AtomicReference<GroupVisibilityUpdatedEvent> event =
+				new AtomicReference<>();
+
+		context.checking(new Expectations() {{
+			oneOf(database).startTransaction();
+			will(returnValue(txn));
+			oneOf(database).containsContact(txn, contactId);
+			will(returnValue(true));
+			oneOf(database).containsGroup(txn, groupId);
+			will(returnValue(true));
+			oneOf(database).getGroupVisibility(txn, contactId, groupId);
+			will(returnValue(VISIBLE));
+			oneOf(database).removeGroupVisibility(txn, contactId, groupId);
+			oneOf(database).commitTransaction(txn);
+			oneOf(eventBus).broadcast(with(any(
+					GroupVisibilityUpdatedEvent.class)));
+			will(new CaptureArgumentAction<>(event,
+					GroupVisibilityUpdatedEvent.class, 0));
+		}});
+		DatabaseComponent db = createDatabaseComponent(database, eventBus,
+				shutdown);
+
+		Transaction transaction = db.startTransaction(false);
+		try {
+			db.setGroupVisibility(transaction, contactId, groupId, INVISIBLE);
+			db.commitTransaction(transaction);
+		} finally {
+			db.endTransaction(transaction);
+		}
+
+		GroupVisibilityUpdatedEvent e = event.get();
+		assertNotNull(e);
+		assertEquals(singletonList(contactId), e.getAffectedContacts());
 	}
 
 	@Test
@@ -1266,8 +1303,8 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 	@Test
 	public void testTransportKeys() throws Exception {
 		TransportKeys transportKeys = createTransportKeys();
-		Map<ContactId, TransportKeys> keys = Collections.singletonMap(
-				contactId, transportKeys);
+		Map<ContactId, TransportKeys> keys =
+				singletonMap(contactId, transportKeys);
 		context.checking(new Expectations() {{
 			// startTransaction()
 			oneOf(database).startTransaction();
@@ -1476,13 +1513,8 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
 			will(returnValue(true));
 			oneOf(database).containsMessage(txn, messageId);
 			will(returnValue(false));
-			oneOf(database).addMessage(txn, message, DELIVERED, true);
-			oneOf(database).getGroupVisibility(txn, groupId);
-			will(returnValue(Collections.singletonList(contactId)));
+			oneOf(database).addMessage(txn, message, DELIVERED, true, null);
 			oneOf(database).mergeMessageMetadata(txn, messageId, metadata);
-			oneOf(database).removeOfferedMessage(txn, contactId, messageId);
-			will(returnValue(false));
-			oneOf(database).addStatus(txn, contactId, messageId, false, false);
 			// addMessageDependencies()
 			oneOf(database).containsMessage(txn, messageId);
 			will(returnValue(true));
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabasePerformanceTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabasePerformanceTest.java
index 7cde0a2333554c67e9f6c2988c3d0cb53c1a72c4..8424c0fec074e6311a28becaf85444d70700011f 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/db/DatabasePerformanceTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/db/DatabasePerformanceTest.java
@@ -563,9 +563,9 @@ public abstract class DatabasePerformanceTest extends BrambleTestCase {
 					Message m = getMessage(g.getId());
 					messages.add(m);
 					State state = State.fromValue(random.nextInt(4));
-					db.addMessage(txn, m, state, random.nextBoolean());
-					db.addStatus(txn, c, m.getId(), random.nextBoolean(),
-							random.nextBoolean());
+					boolean shared = random.nextBoolean();
+					ContactId sender = random.nextBoolean() ? c : null;
+					db.addMessage(txn, m, state, shared, sender);
 					if (random.nextBoolean())
 						db.raiseRequestedFlag(txn, c, m.getId());
 					Metadata mm = getMetadata(METADATA_KEYS_PER_MESSAGE);
@@ -593,7 +593,7 @@ public abstract class DatabasePerformanceTest extends BrambleTestCase {
 			for (int j = 0; j < MESSAGES_PER_GROUP; j++) {
 				Message m = getMessage(g.getId());
 				messages.add(m);
-				db.addMessage(txn, m, DELIVERED, false);
+				db.addMessage(txn, m, DELIVERED, false, null);
 				Metadata mm = getMetadata(METADATA_KEYS_PER_MESSAGE);
 				messageMeta.get(g.getId()).add(mm);
 				db.mergeMessageMetadata(txn, m.getId(), mm);
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java
index a09bc934f40d15fab9cbcd2dd783a404a673d574..f71f54f68c04ef231a3d81b7bed90ac821e4db9d 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java
@@ -57,6 +57,7 @@ import static org.briarproject.bramble.test.TestUtils.getAuthor;
 import static org.briarproject.bramble.test.TestUtils.getLocalAuthor;
 import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
 import static org.briarproject.bramble.test.TestUtils.getRandomId;
+import static org.briarproject.bramble.test.TestUtils.getSecretKey;
 import static org.briarproject.bramble.util.StringUtils.getRandomString;
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
@@ -124,7 +125,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		db.addGroup(txn, group);
 		assertTrue(db.containsGroup(txn, groupId));
 		assertFalse(db.containsMessage(txn, messageId));
-		db.addMessage(txn, message, DELIVERED, true);
+		db.addMessage(txn, message, DELIVERED, true, null);
 		assertTrue(db.containsMessage(txn, messageId));
 		db.commitTransaction(txn);
 		db.close();
@@ -162,7 +163,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 
 		// Add a group and a message
 		db.addGroup(txn, group);
-		db.addMessage(txn, message, DELIVERED, true);
+		db.addMessage(txn, message, DELIVERED, true, null);
 
 		// Removing the group should remove the message
 		assertTrue(db.containsMessage(txn, messageId));
@@ -184,18 +185,11 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 				true, true));
 		db.addGroup(txn, group);
 		db.addGroupVisibility(txn, contactId, groupId, true);
-		db.addMessage(txn, message, DELIVERED, true);
+		db.addMessage(txn, message, DELIVERED, true, null);
 
-		// The message has no status yet, so it 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());
-
-		// Adding a status with seen = false should make the message sendable
-		db.addStatus(txn, contactId, messageId, false, false);
-		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
+		// The contact has not seen the message, so it should be sendable
+		Collection<MessageId> ids =
+				db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
 		assertEquals(Collections.singletonList(messageId), ids);
 		ids = db.getMessagesToOffer(txn, contactId, 100);
 		assertEquals(Collections.singletonList(messageId), ids);
@@ -222,8 +216,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 				true, true));
 		db.addGroup(txn, group);
 		db.addGroupVisibility(txn, contactId, groupId, true);
-		db.addMessage(txn, message, UNKNOWN, true);
-		db.addStatus(txn, contactId, messageId, false, false);
+		db.addMessage(txn, message, UNKNOWN, true, null);
 
 		// The message has not been validated, so it should not be sendable
 		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
@@ -267,8 +260,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
 				true, true));
 		db.addGroup(txn, group);
-		db.addMessage(txn, message, DELIVERED, true);
-		db.addStatus(txn, contactId, messageId, false, false);
+		db.addMessage(txn, message, DELIVERED, true, null);
 
 		// The group is invisible, so the message should not be sendable
 		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
@@ -320,8 +312,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 				true, true));
 		db.addGroup(txn, group);
 		db.addGroupVisibility(txn, contactId, groupId, true);
-		db.addMessage(txn, message, DELIVERED, false);
-		db.addStatus(txn, contactId, messageId, false, false);
+		db.addMessage(txn, message, DELIVERED, false, null);
 
 		// The message is not shared, so it should not be sendable
 		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
@@ -352,8 +343,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 				true, true));
 		db.addGroup(txn, group);
 		db.addGroupVisibility(txn, contactId, groupId, true);
-		db.addMessage(txn, message, DELIVERED, true);
-		db.addStatus(txn, contactId, messageId, false, false);
+		db.addMessage(txn, message, DELIVERED, true, null);
 
 		// The message is sendable, but too large to send
 		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
@@ -383,12 +373,8 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		// Add some messages to ack
 		MessageId messageId1 = new MessageId(getRandomId());
 		Message message1 = new Message(messageId1, groupId, timestamp, raw);
-		db.addMessage(txn, message, DELIVERED, true);
-		db.addStatus(txn, contactId, messageId, false, true);
-		db.raiseAckFlag(txn, contactId, messageId);
-		db.addMessage(txn, message1, DELIVERED, true);
-		db.addStatus(txn, contactId, messageId1, false, true);
-		db.raiseAckFlag(txn, contactId, messageId1);
+		db.addMessage(txn, message, DELIVERED, true, contactId);
+		db.addMessage(txn, message1, DELIVERED, true, contactId);
 
 		// Both message IDs should be returned
 		Collection<MessageId> ids = db.getMessagesToAck(txn, contactId, 1234);
@@ -401,6 +387,14 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		assertEquals(Collections.emptyList(), db.getMessagesToAck(txn,
 				contactId, 1234));
 
+		// Raise the ack flag again
+		db.raiseAckFlag(txn, contactId, messageId);
+		db.raiseAckFlag(txn, contactId, messageId1);
+
+		// Both message IDs should be returned
+		ids = db.getMessagesToAck(txn, contactId, 1234);
+		assertEquals(Arrays.asList(messageId, messageId1), ids);
+
 		db.commitTransaction(txn);
 		db.close();
 	}
@@ -416,8 +410,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 				true, true));
 		db.addGroup(txn, group);
 		db.addGroupVisibility(txn, contactId, groupId, true);
-		db.addMessage(txn, message, DELIVERED, true);
-		db.addStatus(txn, contactId, messageId, false, false);
+		db.addMessage(txn, message, DELIVERED, true, null);
 
 		// Retrieve the message from the database and mark it as sent
 		Collection<MessageId> ids = db.getMessagesToSend(txn, contactId,
@@ -458,7 +451,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		// Storing a message should reduce the free space
 		Connection txn = db.startTransaction();
 		db.addGroup(txn, group);
-		db.addMessage(txn, message, DELIVERED, true);
+		db.addMessage(txn, message, DELIVERED, true, null);
 		db.commitTransaction(txn);
 		assertTrue(db.getFreeSpace() < free);
 
@@ -606,15 +599,14 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add a contact, a group and a message
+		// Add a contact, an invisible group and a message
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthor.getId(),
 				true, true));
 		db.addGroup(txn, group);
-		db.addMessage(txn, message, DELIVERED, true);
-		db.addStatus(txn, contactId, messageId, false, false);
+		db.addMessage(txn, message, DELIVERED, true, null);
 
-		// The group is not visible
+		// The group is not visible so the message should not be visible
 		assertFalse(db.containsVisibleMessage(txn, contactId, messageId));
 
 		db.commitTransaction(txn);
@@ -634,31 +626,31 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 
 		// The group should not be visible to the contact
 		assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId));
-		assertEquals(Collections.emptyList(),
+		assertEquals(Collections.emptyMap(),
 				db.getGroupVisibility(txn, groupId));
 
 		// Make the group visible to the contact
 		db.addGroupVisibility(txn, contactId, groupId, false);
 		assertEquals(VISIBLE, db.getGroupVisibility(txn, contactId, groupId));
-		assertEquals(Collections.singletonList(contactId),
+		assertEquals(Collections.singletonMap(contactId, false),
 				db.getGroupVisibility(txn, groupId));
 
 		// Share the group with the contact
 		db.setGroupVisibility(txn, contactId, groupId, true);
 		assertEquals(SHARED, db.getGroupVisibility(txn, contactId, groupId));
-		assertEquals(Collections.singletonList(contactId),
+		assertEquals(Collections.singletonMap(contactId, true),
 				db.getGroupVisibility(txn, groupId));
 
 		// Unshare the group with the contact
 		db.setGroupVisibility(txn, contactId, groupId, false);
 		assertEquals(VISIBLE, db.getGroupVisibility(txn, contactId, groupId));
-		assertEquals(Collections.singletonList(contactId),
+		assertEquals(Collections.singletonMap(contactId, false),
 				db.getGroupVisibility(txn, groupId));
 
 		// Make the group invisible again
 		db.removeGroupVisibility(txn, contactId, groupId);
 		assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId));
-		assertEquals(Collections.emptyList(),
+		assertEquals(Collections.emptyMap(),
 				db.getGroupVisibility(txn, groupId));
 
 		db.commitTransaction(txn);
@@ -879,8 +871,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		// Remove some of the offered messages and count again
 		List<MessageId> half = ids.subList(0, 5);
 		db.removeOfferedMessages(txn, contactId, half);
-		assertTrue(db.removeOfferedMessage(txn, contactId, ids.get(5)));
-		assertEquals(4, db.countOfferedMessages(txn, contactId));
+		assertEquals(5, db.countOfferedMessages(txn, contactId));
 
 		db.commitTransaction(txn);
 		db.close();
@@ -931,7 +922,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 
 		// Add a group and a message
 		db.addGroup(txn, group);
-		db.addMessage(txn, message, DELIVERED, true);
+		db.addMessage(txn, message, DELIVERED, true, null);
 
 		// Attach some metadata to the message
 		Metadata metadata = new Metadata();
@@ -1002,7 +993,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 
 		// Add a group and a message
 		db.addGroup(txn, group);
-		db.addMessage(txn, message, DELIVERED, true);
+		db.addMessage(txn, message, DELIVERED, true, null);
 
 		// Attach some metadata to the message
 		Metadata metadata = new Metadata();
@@ -1063,8 +1054,8 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 
 		// Add a group and two messages
 		db.addGroup(txn, group);
-		db.addMessage(txn, message, DELIVERED, true);
-		db.addMessage(txn, message1, DELIVERED, true);
+		db.addMessage(txn, message, DELIVERED, true, null);
+		db.addMessage(txn, message1, DELIVERED, true, null);
 
 		// Attach some metadata to the messages
 		Metadata metadata = new Metadata();
@@ -1167,8 +1158,8 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 
 		// Add a group and two messages
 		db.addGroup(txn, group);
-		db.addMessage(txn, message, DELIVERED, true);
-		db.addMessage(txn, message1, DELIVERED, true);
+		db.addMessage(txn, message, DELIVERED, true, null);
+		db.addMessage(txn, message1, DELIVERED, true, null);
 
 		// Attach some metadata to the messages
 		Metadata metadata = new Metadata();
@@ -1242,9 +1233,9 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 
 		// Add a group and some messages
 		db.addGroup(txn, group);
-		db.addMessage(txn, message, PENDING, true);
-		db.addMessage(txn, message1, DELIVERED, true);
-		db.addMessage(txn, message2, INVALID, true);
+		db.addMessage(txn, message, PENDING, true, contactId);
+		db.addMessage(txn, message1, DELIVERED, true, contactId);
+		db.addMessage(txn, message2, INVALID, true, contactId);
 
 		// Add dependencies
 		db.addMessageDependency(txn, groupId, messageId, messageId1);
@@ -1311,7 +1302,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 
 		// Add a group and a message
 		db.addGroup(txn, group);
-		db.addMessage(txn, message, PENDING, true);
+		db.addMessage(txn, message, PENDING, true, contactId);
 
 		// Add a second group
 		GroupId groupId1 = new GroupId(getRandomId());
@@ -1322,7 +1313,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		// Add a message to the second group
 		MessageId messageId1 = new MessageId(getRandomId());
 		Message message1 = new Message(messageId1, groupId1, timestamp, raw);
-		db.addMessage(txn, message1, DELIVERED, true);
+		db.addMessage(txn, message1, DELIVERED, true, contactId);
 
 		// Create an ID for a missing message
 		MessageId messageId2 = new MessageId(getRandomId());
@@ -1330,7 +1321,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 		// Add another message to the first group
 		MessageId messageId3 = new MessageId(getRandomId());
 		Message message3 = new Message(messageId3, groupId, timestamp, raw);
-		db.addMessage(txn, message3, DELIVERED, true);
+		db.addMessage(txn, message3, DELIVERED, true, contactId);
 
 		// Add dependencies between the messages
 		db.addMessageDependency(txn, groupId, messageId, messageId1);
@@ -1377,10 +1368,10 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 
 		// Add a group and some messages with different states
 		db.addGroup(txn, group);
-		db.addMessage(txn, m1, UNKNOWN, true);
-		db.addMessage(txn, m2, INVALID, true);
-		db.addMessage(txn, m3, PENDING, true);
-		db.addMessage(txn, m4, DELIVERED, true);
+		db.addMessage(txn, m1, UNKNOWN, true, contactId);
+		db.addMessage(txn, m2, INVALID, true, contactId);
+		db.addMessage(txn, m3, PENDING, true, contactId);
+		db.addMessage(txn, m4, DELIVERED, true, contactId);
 
 		Collection<MessageId> result;
 
@@ -1414,10 +1405,10 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 
 		// Add a group and some messages
 		db.addGroup(txn, group);
-		db.addMessage(txn, m1, DELIVERED, true);
-		db.addMessage(txn, m2, DELIVERED, false);
-		db.addMessage(txn, m3, DELIVERED, false);
-		db.addMessage(txn, m4, DELIVERED, true);
+		db.addMessage(txn, m1, DELIVERED, true, contactId);
+		db.addMessage(txn, m2, DELIVERED, false, contactId);
+		db.addMessage(txn, m3, DELIVERED, false, contactId);
+		db.addMessage(txn, m4, DELIVERED, true, contactId);
 
 		// Introduce dependencies between the messages
 		db.addMessageDependency(txn, groupId, mId1, mId2);
@@ -1446,8 +1437,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 				true, true));
 		db.addGroup(txn, group);
 		db.addGroupVisibility(txn, contactId, groupId, true);
-		db.addMessage(txn, message, DELIVERED, true);
-		db.addStatus(txn, contactId, messageId, false, false);
+		db.addMessage(txn, message, DELIVERED, true, null);
 
 		// The message should not be sent or seen
 		MessageStatus status = db.getMessageStatus(txn, contactId, messageId);
@@ -1547,8 +1537,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 				true, true));
 		db.addGroup(txn, group);
 		db.addGroupVisibility(txn, contactId, groupId, true);
-		db.addMessage(txn, message, DELIVERED, true);
-		db.addStatus(txn, contactId, messageId, false, false);
+		db.addMessage(txn, message, DELIVERED, true, null);
 
 		// The message should be visible to the contact
 		assertTrue(db.containsVisibleMessage(txn, contactId, messageId));
@@ -1622,7 +1611,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 
 		// Add a group and a message
 		db.addGroup(txn, group);
-		db.addMessage(txn, message, UNKNOWN, false);
+		db.addMessage(txn, message, UNKNOWN, false, contactId);
 
 		// Walk the message through the validation and delivery states
 		assertEquals(UNKNOWN, db.getMessageState(txn, messageId));
@@ -1662,20 +1651,20 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
 	}
 
 	private TransportKeys createTransportKeys() {
-		SecretKey inPrevTagKey = TestUtils.getSecretKey();
-		SecretKey inPrevHeaderKey = TestUtils.getSecretKey();
+		SecretKey inPrevTagKey = getSecretKey();
+		SecretKey inPrevHeaderKey = getSecretKey();
 		IncomingKeys inPrev = new IncomingKeys(inPrevTagKey, inPrevHeaderKey,
 				1, 123, new byte[4]);
-		SecretKey inCurrTagKey = TestUtils.getSecretKey();
-		SecretKey inCurrHeaderKey = TestUtils.getSecretKey();
+		SecretKey inCurrTagKey = getSecretKey();
+		SecretKey inCurrHeaderKey = getSecretKey();
 		IncomingKeys inCurr = new IncomingKeys(inCurrTagKey, inCurrHeaderKey,
 				2, 234, new byte[4]);
-		SecretKey inNextTagKey = TestUtils.getSecretKey();
-		SecretKey inNextHeaderKey = TestUtils.getSecretKey();
+		SecretKey inNextTagKey = getSecretKey();
+		SecretKey inNextHeaderKey = getSecretKey();
 		IncomingKeys inNext = new IncomingKeys(inNextTagKey, inNextHeaderKey,
 				3, 345, new byte[4]);
-		SecretKey outCurrTagKey = TestUtils.getSecretKey();
-		SecretKey outCurrHeaderKey = TestUtils.getSecretKey();
+		SecretKey outCurrTagKey = getSecretKey();
+		SecretKey outCurrHeaderKey = getSecretKey();
 		OutgoingKeys outCurr = new OutgoingKeys(outCurrTagKey, outCurrHeaderKey,
 				2, 456);
 		return new TransportKeys(transportId, inPrev, inCurr, inNext, outCurr);