diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forum/ForumScreen.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forum/ForumScreen.kt
index 803705e23d97b1f39bf4e916d76210732dcbd048..ec14f6f62386a6c789a34cbde6f671c5798407ed 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forum/ForumScreen.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forum/ForumScreen.kt
@@ -20,6 +20,7 @@ package org.briarproject.briar.desktop.forum
 
 import androidx.compose.runtime.Composable
 import org.briarproject.briar.desktop.forum.conversation.ForumDropdownMenu
+import org.briarproject.briar.desktop.forum.sharing.ForumSharingViewModel
 import org.briarproject.briar.desktop.threadedgroup.ThreadedGroupScreen
 import org.briarproject.briar.desktop.viewmodel.viewModel
 
@@ -29,7 +30,8 @@ fun ForumScreen(
 ) = ThreadedGroupScreen(
     strings = ForumStrings,
     viewModel = viewModel,
-    dropdownMenu = { forumSharingViewModel, expanded, onClose, onLeaveForumClick ->
+    dropdownMenu = { sharingViewModel, expanded, onClose, onLeaveForumClick ->
+        val forumSharingViewModel = sharingViewModel as ForumSharingViewModel
         ForumDropdownMenu(forumSharingViewModel, expanded, onClose, onLeaveForumClick)
     }
 )
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forum/sharing/ForumSharingViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forum/sharing/ForumSharingViewModel.kt
index cf041bf7816889f07d81e2651fdb646c5f30b206..98683c9421de7a2bcd53c6a812831b43010936d9 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forum/sharing/ForumSharingViewModel.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forum/sharing/ForumSharingViewModel.kt
@@ -31,7 +31,6 @@ import org.briarproject.bramble.api.event.EventBus
 import org.briarproject.bramble.api.lifecycle.LifecycleManager
 import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent
 import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent
-import org.briarproject.bramble.api.sync.GroupId
 import org.briarproject.briar.api.conversation.ConversationManager
 import org.briarproject.briar.api.forum.ForumSharingManager
 import org.briarproject.briar.api.forum.event.ForumInvitationResponseReceivedEvent
@@ -42,7 +41,7 @@ import org.briarproject.briar.api.sharing.SharingManager.SharingStatus.SHAREABLE
 import org.briarproject.briar.api.sharing.SharingManager.SharingStatus.SHARING
 import org.briarproject.briar.api.sharing.event.ContactLeftShareableEvent
 import org.briarproject.briar.desktop.contact.ContactItem
-import org.briarproject.briar.desktop.contact.ContactsViewModel
+import org.briarproject.briar.desktop.threadedgroup.sharing.ThreadedGroupSharingViewModel
 import org.briarproject.briar.desktop.threading.BriarExecutors
 import org.briarproject.briar.desktop.threading.UiExecutor
 import org.briarproject.briar.desktop.utils.InternationalizationUtils
@@ -61,7 +60,7 @@ class ForumSharingViewModel @Inject constructor(
     lifecycleManager: LifecycleManager,
     db: TransactionManager,
     eventBus: EventBus,
-) : ContactsViewModel(
+) : ThreadedGroupSharingViewModel(
     contactManager,
     authorManager,
     conversationManager,
@@ -76,8 +75,6 @@ class ForumSharingViewModel @Inject constructor(
         private val LOG = KotlinLogging.logger {}
     }
 
-    private var _groupId: GroupId? = null
-
     private val _sharingStatus = mutableStateOf(emptyMap<ContactId, SharingStatus>())
     private val _shareableSelected = mutableStateOf(emptySet<ContactId>())
     private val _sharingMessage = mutableStateOf("")
@@ -107,25 +104,15 @@ class ForumSharingViewModel @Inject constructor(
 
     val buttonEnabled = derivedStateOf { _shareableSelected.value.isNotEmpty() }
 
-    private val _sharingInfo = mutableStateOf(SharingInfo(0, 0))
-    val sharingInfo = _sharingInfo.asState()
-
     override fun onInit() {
         super.onInit()
         loadContacts()
     }
 
-    @UiExecutor
-    fun setGroupId(groupId: GroupId) {
-        if (_groupId == groupId) return
-        _groupId = groupId
-        reload()
-    }
-
-    private fun reload() {
+    override fun reload() {
+        super.reload()
         _shareableSelected.value = emptySet()
         _sharingMessage.value = ""
-        loadSharingStatus()
     }
 
     @UiExecutor
@@ -189,15 +176,7 @@ class ForumSharingViewModel @Inject constructor(
         }
     }
 
-    override fun loadContactsWithinTransaction(txn: Transaction) {
-        super.loadContactsWithinTransaction(txn)
-        if (_groupId != null) loadSharingStatus(txn)
-    }
-
-    private fun loadSharingStatus(): Unit =
-        runOnDbThreadWithTransaction(true, this::loadSharingStatus)
-
-    private fun loadSharingStatus(txn: Transaction) {
+    override fun loadSharingStatus(txn: Transaction) {
         val groupId = _groupId ?: return
         val map = contactManager.getContacts(txn).associate { contact ->
             contact.id to forumSharingManager.getSharingStatus(txn, groupId, contact)
@@ -210,21 +189,4 @@ class ForumSharingViewModel @Inject constructor(
             _sharingInfo.value = SharingInfo(sharing.size, online)
         }
     }
-
-    data class SharingInfo(val total: Int, val online: Int) {
-        fun addContact(connected: Boolean) = copy(
-            total = total + 1,
-            online = if (connected) online + 1 else online
-        )
-
-        fun removeContact(connected: Boolean) = copy(
-            total = total - 1,
-            online = if (connected) online - 1 else online
-        )
-
-        fun updateContactConnected(connected: Boolean) = copy(
-            total = total,
-            online = if (connected) online + 1 else online - 1
-        )
-    }
 }
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/PrivateGroupScreen.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/PrivateGroupScreen.kt
index 286b65482b6923522ead7c550a5578b761e18189..522783d3037f4574d015b901a1f151e8c3f33b05 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/PrivateGroupScreen.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/PrivateGroupScreen.kt
@@ -19,6 +19,8 @@
 package org.briarproject.briar.desktop.privategroup
 
 import androidx.compose.runtime.Composable
+import org.briarproject.briar.desktop.privategroup.conversation.PrivateGroupDropdownMenu
+import org.briarproject.briar.desktop.privategroup.sharing.PrivateGroupSharingViewModel
 import org.briarproject.briar.desktop.threadedgroup.ThreadedGroupScreen
 import org.briarproject.briar.desktop.viewmodel.viewModel
 
@@ -28,8 +30,8 @@ fun PrivateGroupScreen(
 ) = ThreadedGroupScreen(
     strings = PrivateGroupStrings,
     viewModel = viewModel,
-    dropdownMenu = { forumSharingViewModel, expanded, onClose, onLeaveForumClick ->
-        // todo: uncomment when adapted to groups
-        // ForumDropdownMenu(forumSharingViewModel, expanded, onClose, onLeaveForumClick)
+    dropdownMenu = { sharingViewModel, expanded, onClose, onLeaveForumClick ->
+        val privateGroupSharingViewModel = sharingViewModel as PrivateGroupSharingViewModel
+        PrivateGroupDropdownMenu(privateGroupSharingViewModel, expanded, onClose, onLeaveForumClick)
     }
 )
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/conversation/PrivateGroupConversationViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/conversation/PrivateGroupConversationViewModel.kt
index 593bac83d417b810fc612f682adaadc8e5b3479b..501c155c41de3e98b0d898839c156cf1ea14636a 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/conversation/PrivateGroupConversationViewModel.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/conversation/PrivateGroupConversationViewModel.kt
@@ -39,7 +39,7 @@ import org.briarproject.briar.api.client.MessageTracker
 import org.briarproject.briar.api.privategroup.GroupMessageFactory
 import org.briarproject.briar.api.privategroup.JoinMessageHeader
 import org.briarproject.briar.api.privategroup.PrivateGroupManager
-import org.briarproject.briar.desktop.forum.sharing.ForumSharingViewModel
+import org.briarproject.briar.desktop.privategroup.sharing.PrivateGroupSharingViewModel
 import org.briarproject.briar.desktop.threadedgroup.conversation.ThreadedConversationViewModel
 import org.briarproject.briar.desktop.threading.BriarExecutors
 import org.briarproject.briar.desktop.threading.UiExecutor
@@ -47,7 +47,7 @@ import java.lang.Long.max
 import javax.inject.Inject
 
 class PrivateGroupConversationViewModel @Inject constructor(
-    forumSharingViewModel: ForumSharingViewModel,
+    sharingViewModel: PrivateGroupSharingViewModel,
     private val privateGroupManager: PrivateGroupManager,
     private val privateGroupMessageFactory: GroupMessageFactory,
     private val identityManager: IdentityManager,
@@ -58,7 +58,7 @@ class PrivateGroupConversationViewModel @Inject constructor(
     db: TransactionManager,
     eventBus: EventBus,
 ) : ThreadedConversationViewModel(
-    forumSharingViewModel,
+    sharingViewModel,
     briarExecutors,
     lifecycleManager,
     db,
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/conversation/PrivateGroupDropdownMenu.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/conversation/PrivateGroupDropdownMenu.kt
new file mode 100644
index 0000000000000000000000000000000000000000..571b132e8f40a1305548cb274501e518e0924e2b
--- /dev/null
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/conversation/PrivateGroupDropdownMenu.kt
@@ -0,0 +1,84 @@
+/*
+ * Briar Desktop
+ * Copyright (C) 2023 The Briar Project
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package org.briarproject.briar.desktop.privategroup.conversation
+
+import androidx.compose.material.DropdownMenu
+import androidx.compose.material.DropdownMenuItem
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import org.briarproject.briar.desktop.privategroup.sharing.PrivateGroupSharingViewModel
+import org.briarproject.briar.desktop.ui.getInfoDrawerHandler
+import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
+
+@Composable
+fun PrivateGroupDropdownMenu(
+    privateGroupSharingViewModel: PrivateGroupSharingViewModel,
+    expanded: Boolean,
+    onClose: () -> Unit,
+    onLeaveForumClick: () -> Unit,
+) = DropdownMenu(
+    expanded = expanded,
+    onDismissRequest = onClose,
+) {
+    val infoDrawerHandler = getInfoDrawerHandler()
+//    DropdownMenuItem(
+//        onClick = {
+//            onClose()
+//            infoDrawerHandler.open {
+//                ForumSharingActionDrawerContent(
+//                    close = infoDrawerHandler::close,
+//                    viewModel = forumSharingViewModel,
+//                )
+//            }
+//        }
+//    ) {
+//        Text(
+//            i18n("forum.sharing.action.title"),
+//            style = MaterialTheme.typography.body2,
+//        )
+//    }
+//    DropdownMenuItem(
+//        onClick = {
+//            onClose()
+//            infoDrawerHandler.open {
+//                ForumSharingStatusDrawerContent(
+//                    close = infoDrawerHandler::close,
+//                    viewModel = forumSharingViewModel,
+//                )
+//            }
+//        }
+//    ) {
+//        Text(
+//            i18n("forum.sharing.status.title"),
+//            style = MaterialTheme.typography.body2,
+//        )
+//    }
+    DropdownMenuItem(
+        onClick = {
+            onClose()
+            onLeaveForumClick()
+        }
+    ) {
+        Text(
+            i18n("forum.leave.title"),
+            style = MaterialTheme.typography.body2,
+        )
+    }
+}
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/sharing/PrivateGroupSharingViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/sharing/PrivateGroupSharingViewModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..46ea4eedda10ee21b14888a685655bafe50e406f
--- /dev/null
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/sharing/PrivateGroupSharingViewModel.kt
@@ -0,0 +1,184 @@
+/*
+ * Briar Desktop
+ * Copyright (C) 2021-2023 The Briar Project
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package org.briarproject.briar.desktop.privategroup.sharing
+
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.mutableStateOf
+import org.briarproject.bramble.api.connection.ConnectionRegistry
+import org.briarproject.bramble.api.contact.ContactId
+import org.briarproject.bramble.api.contact.ContactManager
+import org.briarproject.bramble.api.db.Transaction
+import org.briarproject.bramble.api.db.TransactionManager
+import org.briarproject.bramble.api.event.Event
+import org.briarproject.bramble.api.event.EventBus
+import org.briarproject.bramble.api.lifecycle.LifecycleManager
+import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent
+import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent
+import org.briarproject.briar.api.conversation.ConversationManager
+import org.briarproject.briar.api.forum.ForumSharingManager
+import org.briarproject.briar.api.forum.event.ForumInvitationResponseReceivedEvent
+import org.briarproject.briar.api.identity.AuthorManager
+import org.briarproject.briar.api.sharing.SharingConstants.MAX_INVITATION_TEXT_LENGTH
+import org.briarproject.briar.api.sharing.SharingManager.SharingStatus
+import org.briarproject.briar.api.sharing.SharingManager.SharingStatus.SHAREABLE
+import org.briarproject.briar.api.sharing.SharingManager.SharingStatus.SHARING
+import org.briarproject.briar.api.sharing.event.ContactLeftShareableEvent
+import org.briarproject.briar.desktop.contact.ContactItem
+import org.briarproject.briar.desktop.threadedgroup.sharing.ThreadedGroupSharingViewModel
+import org.briarproject.briar.desktop.threading.BriarExecutors
+import org.briarproject.briar.desktop.threading.UiExecutor
+import org.briarproject.briar.desktop.utils.InternationalizationUtils
+import org.briarproject.briar.desktop.utils.StringUtils.takeUtf8
+import org.briarproject.briar.desktop.viewmodel.asState
+import org.briarproject.briar.desktop.viewmodel.update
+import javax.inject.Inject
+
+class PrivateGroupSharingViewModel @Inject constructor(
+    private val forumSharingManager: ForumSharingManager,
+    contactManager: ContactManager,
+    authorManager: AuthorManager,
+    conversationManager: ConversationManager,
+    private val connectionRegistry: ConnectionRegistry,
+    briarExecutors: BriarExecutors,
+    lifecycleManager: LifecycleManager,
+    db: TransactionManager,
+    eventBus: EventBus,
+) : ThreadedGroupSharingViewModel(
+    contactManager,
+    authorManager,
+    conversationManager,
+    connectionRegistry,
+    briarExecutors,
+    lifecycleManager,
+    db,
+    eventBus,
+) {
+
+    private val _sharingStatus = mutableStateOf(emptyMap<ContactId, SharingStatus>())
+    private val _shareableSelected = mutableStateOf(emptySet<ContactId>())
+    private val _sharingMessage = mutableStateOf("")
+
+    val currentlySharedWith = derivedStateOf {
+        _contactList.filter { _sharingStatus.value[it.id] == SHARING }
+    }
+
+    data class ShareableContactItem(val status: SharingStatus, val contactItem: ContactItem)
+
+    val contactList = derivedStateOf {
+        _contactList.mapNotNull {
+            _sharingStatus.value[it.id]?.let { status ->
+                ShareableContactItem(status, it)
+            }
+        }.sortedWith(
+            // first all items that are SHAREABLE (false comes before true)
+            // second non-case-sensitive, alphabetical order on displayName
+            compareBy(
+                { it.status != SHAREABLE },
+                { it.contactItem.displayName.lowercase(InternationalizationUtils.locale) }
+            )
+        )
+    }
+
+    val sharingMessage = _sharingMessage.asState()
+
+    val buttonEnabled = derivedStateOf { _shareableSelected.value.isNotEmpty() }
+
+    override fun onInit() {
+        super.onInit()
+        loadContacts()
+    }
+
+    override fun reload() {
+        super.reload()
+        _shareableSelected.value = emptySet()
+        _sharingMessage.value = ""
+    }
+
+    @UiExecutor
+    fun isShareableSelected(shareable: ShareableContactItem) =
+        _shareableSelected.value.contains(shareable.contactItem.id)
+
+    @UiExecutor
+    fun toggleShareable(shareable: ShareableContactItem) =
+        if (isShareableSelected(shareable)) _shareableSelected.value -= shareable.contactItem.id
+        else _shareableSelected.value += shareable.contactItem.id
+
+    @UiExecutor
+    fun setSharingMessage(message: String) {
+        _sharingMessage.value = message.takeUtf8(MAX_INVITATION_TEXT_LENGTH)
+    }
+
+    @UiExecutor
+    fun shareForum() = runOnDbThreadWithTransaction(false) { txn ->
+        val groupId = _groupId ?: return@runOnDbThreadWithTransaction
+        val message = _sharingMessage.value.ifEmpty { null }
+        _shareableSelected.value.forEach { contactId ->
+            forumSharingManager.sendInvitation(txn, groupId, contactId, message)
+        }
+        txn.attach { reload() }
+    }
+
+    @UiExecutor
+    override fun eventOccurred(e: Event) {
+        super.eventOccurred(e)
+
+        val groupId = _groupId ?: return
+        when {
+            e is ForumInvitationResponseReceivedEvent && e.messageHeader.shareableId == groupId -> {
+                if (e.messageHeader.wasAccepted()) {
+                    _sharingStatus.value += e.contactId to SHARING
+                    val connected = connectionRegistry.isConnected(e.contactId)
+                    _sharingInfo.update { addContact(connected) }
+                } else {
+                    _sharingStatus.value += e.contactId to SHAREABLE
+                }
+            }
+
+            e is ContactLeftShareableEvent && e.groupId == groupId -> {
+                _sharingStatus.value += e.contactId to SHAREABLE
+                val connected = connectionRegistry.isConnected(e.contactId)
+                _sharingInfo.update { removeContact(connected) }
+            }
+
+            e is ContactConnectedEvent -> {
+                if (_sharingStatus.value[e.contactId] == SHARING)
+                    _sharingInfo.update { updateContactConnected(true) }
+            }
+
+            e is ContactDisconnectedEvent -> {
+                if (_sharingStatus.value[e.contactId] == SHARING)
+                    _sharingInfo.update { updateContactConnected(false) }
+            }
+        }
+    }
+
+    override fun loadSharingStatus(txn: Transaction) {
+        val groupId = _groupId ?: return
+        val map = contactManager.getContacts(txn).associate { contact ->
+            contact.id to forumSharingManager.getSharingStatus(txn, groupId, contact)
+        }
+        txn.attach {
+            val sharing = map.filterValues { it == SHARING }.keys
+            val online =
+                sharing.fold(0) { acc, it -> if (connectionRegistry.isConnected(it)) acc + 1 else acc }
+            _sharingStatus.value = map
+            _sharingInfo.value = SharingInfo(sharing.size, online)
+        }
+    }
+}
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadedConversationViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadedConversationViewModel.kt
index b14b6456b4f761b9a586e09bd2442017b008c91a..3e5122191d51d39fcdc4490504a93bac2fef2277 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadedConversationViewModel.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadedConversationViewModel.kt
@@ -30,15 +30,15 @@ import org.briarproject.bramble.api.sync.MessageId
 import org.briarproject.briar.api.client.PostHeader
 import org.briarproject.briar.client.MessageTreeImpl
 import org.briarproject.briar.desktop.forum.ForumPostReadEvent
-import org.briarproject.briar.desktop.forum.sharing.ForumSharingViewModel
 import org.briarproject.briar.desktop.threadedgroup.ThreadedGroupItem
+import org.briarproject.briar.desktop.threadedgroup.sharing.ThreadedGroupSharingViewModel
 import org.briarproject.briar.desktop.threading.BriarExecutors
 import org.briarproject.briar.desktop.threading.UiExecutor
 import org.briarproject.briar.desktop.viewmodel.EventListenerDbViewModel
 import org.briarproject.briar.desktop.viewmodel.asState
 
 abstract class ThreadedConversationViewModel(
-    val forumSharingViewModel: ForumSharingViewModel,
+    val sharingViewModel: ThreadedGroupSharingViewModel,
     briarExecutors: BriarExecutors,
     lifecycleManager: LifecycleManager,
     db: TransactionManager,
@@ -61,18 +61,18 @@ abstract class ThreadedConversationViewModel(
         this.onThreadItemLocallyAdded = onThreadItemLocallyAdded
         _threadedGroupItem.value = threadedGroupItem
         _selectedThreadItem.value = null
-        forumSharingViewModel.setGroupId(threadedGroupItem.id)
+        sharingViewModel.setGroupId(threadedGroupItem.id)
         loadThreadItems(threadedGroupItem.id)
     }
 
     override fun onInit() {
         super.onInit()
-        forumSharingViewModel.onEnterComposition()
+        sharingViewModel.onEnterComposition()
     }
 
     override fun onCleared() {
         super.onCleared()
-        forumSharingViewModel.onExitComposition()
+        sharingViewModel.onExitComposition()
     }
 
     protected abstract fun loadThreadItems(txn: Transaction, groupId: GroupId): List<ThreadItem>
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadedGroupConversationScreen.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadedGroupConversationScreen.kt
index 6f5422be11a6939e0339b1d395b745d6da05d56a..cc995cbf2899bbf553bf24fab67c97a46da75fe4 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadedGroupConversationScreen.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadedGroupConversationScreen.kt
@@ -51,10 +51,10 @@ import androidx.compose.ui.text.style.TextOverflow.Companion.Ellipsis
 import androidx.compose.ui.unit.dp
 import org.briarproject.briar.desktop.contact.ContactDropDown.State.CLOSED
 import org.briarproject.briar.desktop.contact.ContactDropDown.State.MAIN
-import org.briarproject.briar.desktop.forum.sharing.ForumSharingViewModel
 import org.briarproject.briar.desktop.threadedgroup.ThreadedGroupCircle
 import org.briarproject.briar.desktop.threadedgroup.ThreadedGroupItem
 import org.briarproject.briar.desktop.threadedgroup.ThreadedGroupStrings
+import org.briarproject.briar.desktop.threadedgroup.sharing.ThreadedGroupSharingViewModel
 import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE
 import org.briarproject.briar.desktop.ui.HorizontalDivider
 import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
@@ -71,7 +71,7 @@ fun ThreadedGroupConversationScreen(
                 ThreadedGroupConversationHeader(
                     strings = strings,
                     threadedGroupItem = groupItem,
-                    forumSharingViewModel = viewModel.forumSharingViewModel,
+                    sharingViewModel = viewModel.sharingViewModel,
                     onGroupDelete = viewModel::deleteGroup,
                     dropdownMenu = dropdownMenu,
                 )
@@ -100,7 +100,7 @@ fun ThreadedGroupConversationScreen(
 private fun ThreadedGroupConversationHeader(
     strings: ThreadedGroupStrings,
     threadedGroupItem: ThreadedGroupItem,
-    forumSharingViewModel: ForumSharingViewModel,
+    sharingViewModel: ThreadedGroupSharingViewModel,
     onGroupDelete: () -> Unit,
     dropdownMenu: ThreadedGroupDropdownMenu,
 ) {
@@ -129,7 +129,7 @@ private fun ThreadedGroupConversationHeader(
                         overflow = Ellipsis,
                         style = MaterialTheme.typography.h2,
                     )
-                    val sharingInfo = forumSharingViewModel.sharingInfo.value
+                    val sharingInfo = sharingViewModel.sharingInfo.value
                     Text(
                         text = strings.sharedWith(sharingInfo.total, sharingInfo.online)
                     )
@@ -142,7 +142,7 @@ private fun ThreadedGroupConversationHeader(
                 modifier = Modifier.align(CenterVertically).padding(end = 16.dp),
             ) {
                 dropdownMenu(
-                    forumSharingViewModel,
+                    sharingViewModel,
                     menuState.value == MAIN,
                     close
                 ) { deleteGroupDialogVisible.value = true }
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadedGroupDropdownMenu.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadedGroupDropdownMenu.kt
index 0e567fc9f54fecdb1eff73c1a93cf7ffbfcb3eed..dc6909d9505934e72a080f532719be1e6502ebd2 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadedGroupDropdownMenu.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/conversation/ThreadedGroupDropdownMenu.kt
@@ -19,10 +19,10 @@
 package org.briarproject.briar.desktop.threadedgroup.conversation
 
 import androidx.compose.runtime.Composable
-import org.briarproject.briar.desktop.forum.sharing.ForumSharingViewModel
+import org.briarproject.briar.desktop.threadedgroup.sharing.ThreadedGroupSharingViewModel
 
 typealias ThreadedGroupDropdownMenu = @Composable (
-    forumSharingViewModel: ForumSharingViewModel,
+    sharingViewModel: ThreadedGroupSharingViewModel,
     expanded: Boolean,
     onClose: () -> Unit,
     onLeaveGroupClick: () -> Unit,
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/sharing/ThreadedGroupSharingViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/sharing/ThreadedGroupSharingViewModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..fe9f1379352e71bdbe3d7c18d2f72a1bddfebea1
--- /dev/null
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threadedgroup/sharing/ThreadedGroupSharingViewModel.kt
@@ -0,0 +1,102 @@
+/*
+ * Briar Desktop
+ * Copyright (C) 2021-2023 The Briar Project
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package org.briarproject.briar.desktop.threadedgroup.sharing
+
+import androidx.compose.runtime.mutableStateOf
+import org.briarproject.bramble.api.connection.ConnectionRegistry
+import org.briarproject.bramble.api.contact.ContactManager
+import org.briarproject.bramble.api.db.Transaction
+import org.briarproject.bramble.api.db.TransactionManager
+import org.briarproject.bramble.api.event.EventBus
+import org.briarproject.bramble.api.lifecycle.LifecycleManager
+import org.briarproject.bramble.api.sync.GroupId
+import org.briarproject.briar.api.conversation.ConversationManager
+import org.briarproject.briar.api.identity.AuthorManager
+import org.briarproject.briar.desktop.contact.ContactsViewModel
+import org.briarproject.briar.desktop.threading.BriarExecutors
+import org.briarproject.briar.desktop.threading.UiExecutor
+import org.briarproject.briar.desktop.viewmodel.asState
+
+abstract class ThreadedGroupSharingViewModel(
+    contactManager: ContactManager,
+    authorManager: AuthorManager,
+    conversationManager: ConversationManager,
+    connectionRegistry: ConnectionRegistry,
+    briarExecutors: BriarExecutors,
+    lifecycleManager: LifecycleManager,
+    db: TransactionManager,
+    eventBus: EventBus,
+) : ContactsViewModel(
+    contactManager,
+    authorManager,
+    conversationManager,
+    connectionRegistry,
+    briarExecutors,
+    lifecycleManager,
+    db,
+    eventBus,
+) {
+    protected var _groupId: GroupId? = null
+
+    protected val _sharingInfo = mutableStateOf(SharingInfo(0, 0))
+    val sharingInfo = _sharingInfo.asState()
+
+    override fun onInit() {
+        super.onInit()
+        loadContacts()
+    }
+
+    @UiExecutor
+    fun setGroupId(groupId: GroupId) {
+        if (_groupId == groupId) return
+        _groupId = groupId
+        reload()
+    }
+
+    protected open fun reload() {
+        loadSharingStatus()
+    }
+
+    override fun loadContactsWithinTransaction(txn: Transaction) {
+        super.loadContactsWithinTransaction(txn)
+        if (_groupId != null) loadSharingStatus(txn)
+    }
+
+    private fun loadSharingStatus(): Unit =
+        runOnDbThreadWithTransaction(true, this::loadSharingStatus)
+
+    protected abstract fun loadSharingStatus(txn: Transaction)
+
+    data class SharingInfo(val total: Int, val online: Int) {
+        fun addContact(connected: Boolean) = copy(
+            total = total + 1,
+            online = if (connected) online + 1 else online
+        )
+
+        fun removeContact(connected: Boolean) = copy(
+            total = total - 1,
+            online = if (connected) online - 1 else online
+        )
+
+        fun updateContactConnected(connected: Boolean) = copy(
+            total = total,
+            online = if (connected) online + 1 else online - 1
+        )
+    }
+}
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModelModule.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModelModule.kt
index 244409ecf8fe86d49cca889a12887ae8b32c6876..aee751faa6747bd25f232a1aa21ca6422da90383 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModelModule.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/ViewModelModule.kt
@@ -32,6 +32,7 @@ import org.briarproject.briar.desktop.login.StartupViewModel
 import org.briarproject.briar.desktop.mailbox.MailboxViewModel
 import org.briarproject.briar.desktop.navigation.SidebarViewModel
 import org.briarproject.briar.desktop.privategroup.PrivateGroupListViewModel
+import org.briarproject.briar.desktop.privategroup.sharing.PrivateGroupSharingViewModel
 import org.briarproject.briar.desktop.settings.SettingsViewModel
 import kotlin.reflect.KClass
 
@@ -85,6 +86,11 @@ abstract class ViewModelModule {
     @ViewModelKey(ForumSharingViewModel::class)
     abstract fun bindForumSharingViewModel(forumSharingViewModel: ForumSharingViewModel): ViewModel
 
+    @Binds
+    @IntoMap
+    @ViewModelKey(PrivateGroupSharingViewModel::class)
+    abstract fun bindPrivateGroupSharingViewModel(privateGroupSharingViewModel: PrivateGroupSharingViewModel): ViewModel
+
     @Binds
     @IntoMap
     @ViewModelKey(MailboxViewModel::class)