From d04dda1566725a100fb00b145286061e78e01876 Mon Sep 17 00:00:00 2001
From: Torsten Grote <t@grobox.de>
Date: Mon, 5 Dec 2016 16:39:34 -0200
Subject: [PATCH] Add sharing information to toolbar subtitle of blogs

The toolbar subtitle shows information about how many contacts the
current blog is shared with and how many of those are online.
---
 .../briar/android/blog/BlogActivity.java      |  31 +++++-
 .../briar/android/blog/BlogController.java    |  17 +++
 .../android/blog/BlogControllerImpl.java      |  74 +++++++++++-
 .../briar/android/blog/BlogFragment.java      |  66 ++++++++++-
 .../briar/android/blog/BlogModule.java        |  10 ++
 .../android/controller/SharingController.java |  71 ++++++++++++
 .../controller/SharingControllerImpl.java     | 105 ++++++++++++++++++
 briar-android/src/main/res/values/strings.xml |   1 +
 .../api/sharing/event/ShareableLeftEvent.java |  31 ++++++
 .../briar/sharing/BlogSharingManagerImpl.java |   3 +-
 .../sharing/ForumSharingManagerImpl.java      |   4 +-
 .../InvitationReceivedEventFactory.java       |   4 +-
 .../briar/sharing/SharingManagerImpl.java     |   7 ++
 13 files changed, 411 insertions(+), 13 deletions(-)
 create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/controller/SharingController.java
 create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/controller/SharingControllerImpl.java
 create mode 100644 briar-api/src/main/java/org/briarproject/briar/api/sharing/event/ShareableLeftEvent.java

diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogActivity.java
index 700ecf40ae..ab6c7cc0c6 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogActivity.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogActivity.java
@@ -2,38 +2,60 @@ package org.briarproject.briar.android.blog;
 
 import android.content.Intent;
 import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.view.View;
 
+import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
+import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.briar.R;
 import org.briarproject.briar.android.activity.ActivityComponent;
 import org.briarproject.briar.android.activity.BriarActivity;
 import org.briarproject.briar.android.blog.BlogPostAdapter.OnBlogPostClickListener;
 import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
+import org.briarproject.briar.android.sharing.BlogSharingStatusActivity;
 
 import javax.inject.Inject;
 
+@MethodsNotNullByDefault
+@ParametersNotNullByDefault
 public class BlogActivity extends BriarActivity implements
 		OnBlogPostClickListener, BaseFragmentListener {
 
-	static final int REQUEST_WRITE_POST = 1;
-	static final int REQUEST_SHARE = 2;
+	static final int REQUEST_WRITE_POST = 2;
+	static final int REQUEST_SHARE = 3;
 
 	@Inject
 	BlogController blogController;
 
 	@Override
-	public void onCreate(Bundle state) {
+	public void onCreate(@Nullable Bundle state) {
 		super.onCreate(state);
 
 		// GroupId from Intent
 		Intent i = getIntent();
 		byte[] b = i.getByteArrayExtra(GROUP_ID);
 		if (b == null) throw new IllegalStateException("No group ID in intent");
-		GroupId groupId = new GroupId(b);
+		final GroupId groupId = new GroupId(b);
 		blogController.setGroupId(groupId);
 
 		setContentView(R.layout.activity_fragment_container);
 
+		// Open Sharing Status on ActionBar click
+		View actionBar = findViewById(R.id.action_bar);
+		if (actionBar != null) {
+			actionBar.setOnClickListener(
+					new View.OnClickListener() {
+						@Override
+						public void onClick(View v) {
+							Intent i = new Intent(BlogActivity.this,
+									BlogSharingStatusActivity.class);
+							i.putExtra(GROUP_ID, groupId.getBytes());
+							startActivity(i);
+						}
+					});
+		}
+
 		if (state == null) {
 			BlogFragment f = BlogFragment.newInstance(groupId);
 			getSupportFragmentManager().beginTransaction()
@@ -59,4 +81,5 @@ public class BlogActivity extends BriarActivity implements
 	@Override
 	public void onFragmentCreated(String tag) {
 	}
+
 }
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogController.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogController.java
index c58e41e74e..e67450474e 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogController.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogController.java
@@ -1,5 +1,9 @@
 package org.briarproject.briar.android.blog;
 
+import android.support.annotation.UiThread;
+
+import org.briarproject.bramble.api.contact.Contact;
+import org.briarproject.bramble.api.contact.ContactId;
 import org.briarproject.bramble.api.db.DbException;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.GroupId;
@@ -13,6 +17,8 @@ public interface BlogController extends BaseController {
 
 	void setGroupId(GroupId g);
 
+	void setBlogSharingListener(BlogSharingListener listener);
+
 	void loadBlogPosts(
 			ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler);
 
@@ -23,4 +29,15 @@ public interface BlogController extends BaseController {
 
 	void deleteBlog(ResultExceptionHandler<Void, DbException> handler);
 
+	void loadSharingContacts(
+			ResultExceptionHandler<Collection<ContactId>, DbException> handler);
+
+	interface BlogSharingListener extends BlogListener {
+		@UiThread
+		void onBlogInvitationAccepted(ContactId c);
+
+		@UiThread
+		void onBlogLeft(ContactId c);
+	}
+
 }
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogControllerImpl.java
index 43877cc3df..f200702eff 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogControllerImpl.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogControllerImpl.java
@@ -2,6 +2,8 @@ package org.briarproject.briar.android.blog;
 
 import android.app.Activity;
 
+import org.briarproject.bramble.api.contact.Contact;
+import org.briarproject.bramble.api.contact.ContactId;
 import org.briarproject.bramble.api.db.DatabaseExecutor;
 import org.briarproject.bramble.api.db.DbException;
 import org.briarproject.bramble.api.event.Event;
@@ -20,8 +22,13 @@ import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
 import org.briarproject.briar.api.android.AndroidNotificationManager;
 import org.briarproject.briar.api.blog.Blog;
 import org.briarproject.briar.api.blog.BlogManager;
+import org.briarproject.briar.api.blog.BlogSharingManager;
+import org.briarproject.briar.api.blog.event.BlogInvitationResponseReceivedEvent;
 import org.briarproject.briar.api.blog.event.BlogPostAddedEvent;
+import org.briarproject.briar.api.sharing.InvitationResponse;
+import org.briarproject.briar.api.sharing.event.ShareableLeftEvent;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.concurrent.Executor;
 import java.util.logging.Logger;
@@ -39,15 +46,19 @@ class BlogControllerImpl extends BaseControllerImpl
 	private static final Logger LOG =
 			Logger.getLogger(BlogControllerImpl.class.getName());
 
+	private final BlogSharingManager blogSharingManager;
 	private volatile GroupId groupId = null;
+	private volatile BlogSharingListener listener;
 
 	@Inject
 	BlogControllerImpl(@DatabaseExecutor Executor dbExecutor,
 			LifecycleManager lifecycleManager, EventBus eventBus,
 			AndroidNotificationManager notificationManager,
-			IdentityManager identityManager, BlogManager blogManager) {
+			IdentityManager identityManager, BlogManager blogManager,
+			BlogSharingManager blogSharingManager) {
 		super(dbExecutor, lifecycleManager, eventBus, notificationManager,
 				identityManager, blogManager);
+		this.blogSharingManager = blogSharingManager;
 	}
 
 	@Override
@@ -76,6 +87,12 @@ class BlogControllerImpl extends BaseControllerImpl
 		groupId = g;
 	}
 
+	@Override
+	public void setBlogSharingListener(BlogSharingListener listener) {
+		super.setBlogListener(listener);
+		this.listener = listener;
+	}
+
 	@Override
 	public void eventOccurred(Event e) {
 		if (groupId == null) throw new IllegalStateException();
@@ -85,6 +102,20 @@ class BlogControllerImpl extends BaseControllerImpl
 				LOG.info("Blog post added");
 				onBlogPostAdded(b.getHeader(), b.isLocal());
 			}
+		} else if (e instanceof BlogInvitationResponseReceivedEvent) {
+			BlogInvitationResponseReceivedEvent b =
+					(BlogInvitationResponseReceivedEvent) e;
+			InvitationResponse r = b.getResponse();
+			if (r.getGroupId().equals(groupId) && r.wasAccepted()) {
+				LOG.info("Blog invitation accepted");
+				onBlogInvitationAccepted(b.getContactId());
+			}
+		} else if (e instanceof ShareableLeftEvent) {
+			ShareableLeftEvent s = (ShareableLeftEvent) e;
+			if (s.getGroupId().equals(groupId)) {
+				LOG.info("Blog left");
+				onBlogLeft(s.getContactId());
+			}
 		} else if (e instanceof GroupRemovedEvent) {
 			GroupRemovedEvent g = (GroupRemovedEvent) e;
 			if (g.getGroup().getId().equals(groupId)) {
@@ -94,6 +125,24 @@ class BlogControllerImpl extends BaseControllerImpl
 		}
 	}
 
+	private void onBlogInvitationAccepted(final ContactId c) {
+		listener.runOnUiThreadUnlessDestroyed(new Runnable() {
+			@Override
+			public void run() {
+				listener.onBlogInvitationAccepted(c);
+			}
+		});
+	}
+
+	private void onBlogLeft(final ContactId c) {
+		listener.runOnUiThreadUnlessDestroyed(new Runnable() {
+			@Override
+			public void run() {
+				listener.onBlogLeft(c);
+			}
+		});
+	}
+
 	@Override
 	public void loadBlogPosts(
 			final ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler) {
@@ -159,4 +208,27 @@ class BlogControllerImpl extends BaseControllerImpl
 		});
 	}
 
+	@Override
+	public void loadSharingContacts(
+			final ResultExceptionHandler<Collection<ContactId>, DbException> handler) {
+		if (groupId == null) throw new IllegalStateException();
+		runOnDbThread(new Runnable() {
+			@Override
+			public void run() {
+				try {
+					Collection<Contact> contacts =
+							blogSharingManager.getSharedWith(groupId);
+					Collection<ContactId> contactIds =
+							new ArrayList<>(contacts.size());
+					for (Contact c : contacts) contactIds.add(c.getId());
+					handler.onResult(contactIds);
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+					handler.onException(e);
+				}
+			}
+		});
+	}
+
 }
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java
index 10dd6d7201..587a46ba0c 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogFragment.java
@@ -7,6 +7,7 @@ import android.support.annotation.UiThread;
 import android.support.design.widget.Snackbar;
 import android.support.v4.app.ActivityOptionsCompat;
 import android.support.v4.content.ContextCompat;
+import android.support.v7.app.ActionBar;
 import android.support.v7.app.AlertDialog;
 import android.support.v7.widget.LinearLayoutManager;
 import android.view.LayoutInflater;
@@ -17,6 +18,7 @@ import android.view.View;
 import android.view.ViewGroup;
 import android.widget.Toast;
 
+import org.briarproject.bramble.api.contact.ContactId;
 import org.briarproject.bramble.api.db.DbException;
 import org.briarproject.bramble.api.identity.Author;
 import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
@@ -24,8 +26,10 @@ import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.briar.R;
 import org.briarproject.briar.android.activity.ActivityComponent;
-import org.briarproject.briar.android.blog.BaseController.BlogListener;
+import org.briarproject.briar.android.activity.BriarActivity;
+import org.briarproject.briar.android.blog.BlogController.BlogSharingListener;
 import org.briarproject.briar.android.blog.BlogPostAdapter.OnBlogPostClickListener;
+import org.briarproject.briar.android.controller.SharingController;
 import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
 import org.briarproject.briar.android.fragment.BaseFragment;
 import org.briarproject.briar.android.sharing.BlogSharingStatusActivity;
@@ -46,17 +50,20 @@ import static android.widget.Toast.LENGTH_SHORT;
 import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
 import static org.briarproject.briar.android.blog.BlogActivity.REQUEST_SHARE;
 import static org.briarproject.briar.android.blog.BlogActivity.REQUEST_WRITE_POST;
+import static org.briarproject.briar.android.controller.SharingController.SharingListener;
 
 @UiThread
 @MethodsNotNullByDefault
 @ParametersNotNullByDefault
-public class BlogFragment extends BaseFragment implements
-		BlogListener {
+public class BlogFragment extends BaseFragment
+		implements BlogSharingListener, SharingListener {
 
 	private final static String TAG = BlogFragment.class.getName();
 
 	@Inject
 	BlogController blogController;
+	@Inject
+	SharingController sharingController;
 
 	private GroupId groupId;
 	private BlogPostAdapter adapter;
@@ -101,13 +108,16 @@ public class BlogFragment extends BaseFragment implements
 	@Override
 	public void injectFragment(ActivityComponent component) {
 		component.inject(this);
-		blogController.setBlogListener(this);
+		blogController.setBlogSharingListener(this);
+		sharingController.setSharingListener(this);
 	}
 
 	@Override
 	public void onStart() {
 		super.onStart();
+		sharingController.onStart();
 		loadBlog();
+		loadSharedContacts();
 		loadBlogPosts(false);
 		list.startPeriodicUpdate();
 	}
@@ -115,6 +125,7 @@ public class BlogFragment extends BaseFragment implements
 	@Override
 	public void onStop() {
 		super.onStop();
+		sharingController.onStop();
 		list.stopPeriodicUpdate();
 	}
 
@@ -255,6 +266,52 @@ public class BlogFragment extends BaseFragment implements
 		getActivity().setTitle(title);
 	}
 
+	private void loadSharedContacts() {
+		blogController.loadSharingContacts(
+				new UiResultExceptionHandler<Collection<ContactId>, DbException>(this) {
+					@Override
+					public void onResultUi(Collection<ContactId> contacts) {
+						sharingController.addAll(contacts);
+						int online = sharingController.getOnlineCount();
+						setToolbarSubTitle(contacts.size(), online);
+					}
+
+					@Override
+					public void onExceptionUi(DbException exception) {
+						// TODO: Decide how to handle errors in the UI
+						finish();
+					}
+				});
+	}
+
+	@Override
+	public void onBlogInvitationAccepted(ContactId c) {
+		sharingController.add(c);
+		setToolbarSubTitle(sharingController.getTotalCount(),
+				sharingController.getOnlineCount());
+	}
+
+	@Override
+	public void onBlogLeft(ContactId c) {
+		sharingController.remove(c);
+		setToolbarSubTitle(sharingController.getTotalCount(),
+				sharingController.getOnlineCount());
+	}
+
+	@Override
+	public void onSharingInfoUpdated(int total, int online) {
+		setToolbarSubTitle(total, online);
+	}
+
+	private void setToolbarSubTitle(int total, int online) {
+		ActionBar actionBar =
+				((BriarActivity) getActivity()).getSupportActionBar();
+		if (actionBar != null) {
+			actionBar.setSubtitle(
+					getString(R.string.shared_with, total, online));
+		}
+	}
+
 	private void showWriteButton() {
 		isMyBlog = true;
 		if (writeButton != null)
@@ -327,4 +384,5 @@ public class BlogFragment extends BaseFragment implements
 	public void onBlogRemoved() {
 		finish();
 	}
+
 }
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogModule.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogModule.java
index 01e80dcb64..e2db1a7cf2 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogModule.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogModule.java
@@ -2,6 +2,8 @@ package org.briarproject.briar.android.blog;
 
 import org.briarproject.briar.android.activity.ActivityScope;
 import org.briarproject.briar.android.activity.BaseActivity;
+import org.briarproject.briar.android.controller.SharingController;
+import org.briarproject.briar.android.controller.SharingControllerImpl;
 
 import dagger.Module;
 import dagger.Provides;
@@ -22,4 +24,12 @@ public class BlogModule {
 	FeedController provideFeedController(FeedControllerImpl feedController) {
 		return feedController;
 	}
+
+	@ActivityScope
+	@Provides
+	SharingController provideSharingController(
+			SharingControllerImpl sharingController) {
+		return sharingController;
+	}
+
 }
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/controller/SharingController.java b/briar-android/src/main/java/org/briarproject/briar/android/controller/SharingController.java
new file mode 100644
index 0000000000..81d9ce794c
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/controller/SharingController.java
@@ -0,0 +1,71 @@
+package org.briarproject.briar.android.controller;
+
+import android.support.annotation.UiThread;
+
+import org.briarproject.bramble.api.contact.ContactId;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.briar.android.DestroyableContext;
+
+import java.util.Collection;
+
+@NotNullByDefault
+public interface SharingController {
+
+	/**
+	 * Sets the listener that is called when contacts go on or offline.
+	 */
+	@UiThread
+	void setSharingListener(SharingListener listener);
+
+	/**
+	 * Call this when your lifecycle starts,
+	 * so the listener will be called when information changes.
+	 */
+	@UiThread
+	void onStart();
+
+	/**
+	 * Call this when your lifecycle stops,
+	 * so that the controller knows it can stops listening to events.
+	 */
+	@UiThread
+	void onStop();
+
+	/**
+	 * Adds one contact to be tracked.
+	 */
+	@UiThread
+	void add(ContactId c);
+
+	/**
+	 * Adds a collection of contacts to be tracked.
+	 */
+	@UiThread
+	void addAll(Collection<ContactId> contacts);
+
+	/**
+	 * Call this when the contact identified by c is no longer sharing
+	 * the given group identified by GroupId g.
+	 */
+	@UiThread
+	void remove(ContactId c);
+
+	/**
+	 * Returns the number of online contacts.
+	 */
+	@UiThread
+	int getOnlineCount();
+
+	/**
+	 * Returns the total number of contacts that have been added.
+	 */
+	@UiThread
+	int getTotalCount();
+
+	interface SharingListener extends DestroyableContext {
+		@UiThread
+		void onSharingInfoUpdated(int total, int online);
+	}
+
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/controller/SharingControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/controller/SharingControllerImpl.java
new file mode 100644
index 0000000000..87afdfe521
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/controller/SharingControllerImpl.java
@@ -0,0 +1,105 @@
+package org.briarproject.briar.android.controller;
+
+import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
+
+import org.briarproject.bramble.api.contact.ContactId;
+import org.briarproject.bramble.api.event.Event;
+import org.briarproject.bramble.api.event.EventBus;
+import org.briarproject.bramble.api.event.EventListener;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.plugin.ConnectionRegistry;
+import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent;
+import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.inject.Inject;
+
+@UiThread
+@NotNullByDefault
+public class SharingControllerImpl implements SharingController, EventListener {
+
+	private final EventBus eventBus;
+	private final ConnectionRegistry connectionRegistry;
+
+	@Nullable
+	private SharingListener listener;
+	private Set<ContactId> contacts = new HashSet<>();
+
+	@Inject
+	SharingControllerImpl(EventBus eventBus,
+			ConnectionRegistry connectionRegistry) {
+		this.eventBus = eventBus;
+		this.connectionRegistry = connectionRegistry;
+	}
+
+	@Override
+	public void setSharingListener(SharingListener listener) {
+		this.listener = listener;
+	}
+
+	@Override
+	public void onStart() {
+		eventBus.addListener(this);
+	}
+
+	@Override
+	public void onStop() {
+		eventBus.removeListener(this);
+	}
+
+	@Override
+	public void eventOccurred(Event e) {
+		if (e instanceof ContactConnectedEvent) {
+			setConnected(((ContactConnectedEvent) e).getContactId());
+		} else if (e instanceof ContactDisconnectedEvent) {
+			setConnected(((ContactDisconnectedEvent) e).getContactId());
+		}
+	}
+
+	private void setConnected(final ContactId c) {
+		if (listener == null) return;
+		listener.runOnUiThreadUnlessDestroyed(new Runnable() {
+			@Override
+			public void run() {
+				if (contacts.contains(c)) {
+					int online = getOnlineCount();
+					listener.onSharingInfoUpdated(contacts.size(), online);
+				}
+			}
+		});
+	}
+
+	@Override
+	public void addAll(Collection<ContactId> c) {
+		contacts.addAll(c);
+	}
+
+	@Override
+	public void add(ContactId c) {
+		contacts.add(c);
+	}
+
+	@Override
+	public void remove(ContactId c) {
+		contacts.remove(c);
+	}
+
+	@Override
+	public int getOnlineCount() {
+		int online = 0;
+		for (ContactId c : contacts) {
+			if (connectionRegistry.isConnected(c)) online++;
+		}
+		return online;
+	}
+
+	@Override
+	public int getTotalCount() {
+		return contacts.size();
+	}
+
+}
diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml
index 843f13df0d..4fde00b0fd 100644
--- a/briar-android/src/main/res/values/strings.xml
+++ b/briar-android/src/main/res/values/strings.xml
@@ -259,6 +259,7 @@
 	<string name="forum_invitation_response_declined_received">%s declined the forum invitation.</string>
 
 	<string name="sharing_status">Sharing Status</string>
+	<string name="shared_with">Shared with %1$d (%2$d online)</string>
 	<plurals name="forums_shared">
 		<item quantity="one">%d forum shared by contacts</item>
 		<item quantity="other">%d forums shared by contacts</item>
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/sharing/event/ShareableLeftEvent.java b/briar-api/src/main/java/org/briarproject/briar/api/sharing/event/ShareableLeftEvent.java
new file mode 100644
index 0000000000..0548ea0028
--- /dev/null
+++ b/briar-api/src/main/java/org/briarproject/briar/api/sharing/event/ShareableLeftEvent.java
@@ -0,0 +1,31 @@
+package org.briarproject.briar.api.sharing.event;
+
+
+import org.briarproject.bramble.api.contact.ContactId;
+import org.briarproject.bramble.api.event.Event;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+public class ShareableLeftEvent extends Event {
+
+	private final GroupId groupId;
+	private final ContactId contactId;
+
+	public ShareableLeftEvent(GroupId groupId, ContactId contactId) {
+		this.groupId = groupId;
+		this.contactId = contactId;
+	}
+
+	public GroupId getGroupId() {
+		return groupId;
+	}
+
+	public ContactId getContactId() {
+		return contactId;
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/BlogSharingManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/BlogSharingManagerImpl.java
index ba160b2b57..ba56908d8f 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/BlogSharingManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/BlogSharingManagerImpl.java
@@ -357,7 +357,8 @@ class BlogSharingManagerImpl extends
 				throw new IllegalStateException("No responseId");
 			BlogInvitationResponse response =
 					new BlogInvitationResponse(responseId,
-							localState.getSessionId(), localState.getGroupId(),
+							localState.getSessionId(),
+							localState.getShareableId(),
 							localState.getContactId(), accept, time, false,
 							false, false, false);
 			return new BlogInvitationResponseReceivedEvent(c, response);
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/ForumSharingManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/ForumSharingManagerImpl.java
index e564f7bcc6..84363a7089 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/ForumSharingManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/ForumSharingManagerImpl.java
@@ -287,8 +287,8 @@ class ForumSharingManagerImpl extends
 				throw new IllegalStateException("No responseId");
 			ForumInvitationResponse response = new ForumInvitationResponse(
 					responseId, localState.getSessionId(),
-					localState.getGroupId(), localState.getContactId(), accept,
-					time, false, false, false, false);
+					localState.getShareableId(), localState.getContactId(),
+					accept, time, false, false, false, false);
 			return new ForumInvitationResponseReceivedEvent(name, c, response);
 		}
 	}
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/InvitationReceivedEventFactory.java b/briar-core/src/main/java/org/briarproject/briar/sharing/InvitationReceivedEventFactory.java
index b440daf8cf..9c1e3f4e3c 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/InvitationReceivedEventFactory.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/InvitationReceivedEventFactory.java
@@ -3,8 +3,10 @@ package org.briarproject.briar.sharing;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.briar.api.sharing.event.InvitationRequestReceivedEvent;
 
+import javax.annotation.Nullable;
+
 @NotNullByDefault
 interface InvitationReceivedEventFactory<IS extends InviteeSessionState, IR extends InvitationRequestReceivedEvent> {
 
-	IR build(IS localState, long time, String msg);
+	IR build(IS localState, long time, @Nullable String msg);
 }
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingManagerImpl.java
index c1d54f4f9a..9589006290 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingManagerImpl.java
@@ -39,6 +39,7 @@ import org.briarproject.briar.api.sharing.SharingInvitationItem;
 import org.briarproject.briar.api.sharing.SharingManager;
 import org.briarproject.briar.api.sharing.event.InvitationRequestReceivedEvent;
 import org.briarproject.briar.api.sharing.event.InvitationResponseReceivedEvent;
+import org.briarproject.briar.api.sharing.event.ShareableLeftEvent;
 import org.briarproject.briar.client.ConversationClientImpl;
 
 import java.io.IOException;
@@ -897,9 +898,15 @@ abstract class SharingManagerImpl<S extends Shareable, I extends Invitation, IS
 		} else if (task == TASK_UNSHARE_SHAREABLE_SHARED_BY_US) {
 			db.setGroupVisibility(txn, contactId, f.getId(), INVISIBLE);
 			removeFromList(txn, groupId, SHARED_BY_US, f);
+			// broadcast event informing UI that contact has left the group
+			ShareableLeftEvent e = new ShareableLeftEvent(f.getId(), contactId);
+			txn.attach(e);
 		} else if (task == TASK_UNSHARE_SHAREABLE_SHARED_WITH_US) {
 			db.setGroupVisibility(txn, contactId, f.getId(), INVISIBLE);
 			removeFromList(txn, groupId, SHARED_WITH_US, f);
+			// broadcast event informing UI that contact has left the group
+			ShareableLeftEvent e = new ShareableLeftEvent(f.getId(), contactId);
+			txn.attach(e);
 		}
 	}
 
-- 
GitLab