diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/AddForumDialog.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/AddForumDialog.kt
index 529e80f028af236e5efd9cd133c19a7693c075ac..d3d9a0c373dbedd8bd0b75eedc82d7e6d8422713 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/AddForumDialog.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/AddForumDialog.kt
@@ -35,7 +35,6 @@ import androidx.compose.material.TextButton
 import androidx.compose.material.rememberScaffoldState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.State
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.saveable.rememberSaveable
@@ -56,16 +55,16 @@ import java.awt.Dimension
 
 fun main() = preview {
     val visible = mutableStateOf(true)
-    AddForumDialog(visible, {}, { visible.value = false })
+    AddForumDialog(visible.value, {}, { visible.value = false })
 }
 
 @Composable
 fun AddForumDialog(
-    visible: State<Boolean>,
+    visible: Boolean,
     onCreate: (String) -> Unit,
     onCancelButtonClicked: () -> Unit,
 ) {
-    if (!visible.value) return
+    if (!visible) return
     Dialog(
         title = i18n("forum.add.title"),
         onCloseRequest = onCancelButtonClicked,
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumScreen.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumScreen.kt
index 91ce4ac692c7b7e7abddf20f79cb38e74f012d80..11754834928551376dfb16d12dfdba3769b692cd 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumScreen.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumScreen.kt
@@ -41,7 +41,7 @@ fun ForumScreen(
 ) {
     val addDialogVisible = remember { mutableStateOf(false) }
     AddForumDialog(
-        visible = addDialogVisible,
+        visible = addDialogVisible.value,
         onCreate = { name ->
             viewModel.createForum(name)
             addDialogVisible.value = false
@@ -54,9 +54,9 @@ fun ForumScreen(
     } else {
         Row(modifier = Modifier.fillMaxWidth()) {
             GroupListComposable(
-                list = viewModel.forumList,
+                list = viewModel.forumList.value,
                 isSelected = viewModel::isSelected,
-                filterBy = viewModel.filterBy,
+                filterBy = viewModel.filterBy.value,
                 onFilterSet = viewModel::setFilterBy,
                 onGroupItemSelected = viewModel::selectGroup,
                 onAddButtonClicked = { addDialogVisible.value = true },
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumViewModel.kt
index 5b3e28d0833350cf018c3103d346acf053a84f6e..9b05d6db6c3a5fa993418985a3c86c5903c5b841 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumViewModel.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumViewModel.kt
@@ -21,48 +21,22 @@ package org.briarproject.briar.desktop.forums
 import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.mutableStateListOf
 import androidx.compose.runtime.mutableStateOf
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.DelicateCoroutinesApi
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import org.briarproject.bramble.api.crypto.CryptoExecutor
 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.identity.IdentityManager
-import org.briarproject.bramble.api.identity.LocalAuthor
 import org.briarproject.bramble.api.lifecycle.LifecycleManager
 import org.briarproject.bramble.api.sync.GroupId
-import org.briarproject.bramble.api.sync.MessageId
 import org.briarproject.bramble.api.sync.event.GroupAddedEvent
 import org.briarproject.bramble.api.sync.event.GroupRemovedEvent
-import org.briarproject.bramble.api.system.Clock
-import org.briarproject.briar.api.client.MessageTracker.GroupCount
 import org.briarproject.briar.api.forum.ForumManager
-import org.briarproject.briar.client.MessageTreeImpl
 import org.briarproject.briar.desktop.threading.BriarExecutors
-import org.briarproject.briar.desktop.threading.UiExecutor
 import org.briarproject.briar.desktop.utils.clearAndAddAll
 import org.briarproject.briar.desktop.viewmodel.EventListenerDbViewModel
 import org.briarproject.briar.desktop.viewmodel.asState
-import java.lang.Long.max
 import javax.inject.Inject
 
-sealed class PostsState
-object Loading : PostsState()
-class Loaded(
-    val messageTree: MessageTreeImpl<ThreadItem>,
-    val scrollTo: MessageId? = null,
-) : PostsState() {
-    val posts: MutableList<ThreadItem> get() = messageTree.depthFirstOrder()
-}
-
 class ForumViewModel @Inject constructor(
     private val forumManager: ForumManager,
-    private val identityManager: IdentityManager,
-    private val clock: Clock,
-    @CryptoExecutor private val cryptoDispatcher: CoroutineDispatcher,
     briarExecutors: BriarExecutors,
     lifecycleManager: LifecycleManager,
     db: TransactionManager,
@@ -85,9 +59,6 @@ class ForumViewModel @Inject constructor(
     private val _filterBy = mutableStateOf("")
     val filterBy = _filterBy.asState()
 
-    private val _posts = mutableStateOf<PostsState>(Loading)
-    val posts = _posts.asState()
-
     override fun onInit() {
         super.onInit()
         loadGroups()
@@ -124,59 +95,11 @@ class ForumViewModel @Inject constructor(
 
     fun selectGroup(groupItem: GroupItem) {
         _selectedGroupItem.value = groupItem
-        loadPosts(groupItem.id)
     }
 
     fun isSelected(groupId: GroupId) = _selectedGroupItem.value?.id == groupId
 
-    private fun loadPosts(groupId: GroupId) {
-        _posts.value = Loading
-        runOnDbThreadWithTransaction(true) { txn ->
-            val items = forumManager.getPostHeaders(txn, groupId).map { header ->
-                ForumPostItem(header, forumManager.getPostText(txn, header.id))
-            }
-            val tree = MessageTreeImpl<ThreadItem>().apply { add(items) }
-            txn.attach {
-                _posts.value = Loaded(tree)
-            }
-        }
-    }
-
     fun setFilterBy(filter: String) {
         _filterBy.value = filter
     }
-
-    @OptIn(DelicateCoroutinesApi::class)
-    fun createPost(groupItem: GroupItem, text: String, parentId: MessageId?) = GlobalScope.launch {
-        val author = runOnDbThreadWithTransaction<LocalAuthor>(false) { txn ->
-            identityManager.getLocalAuthor(txn)
-        }
-        val count = runOnDbThreadWithTransaction<GroupCount>(false) { txn ->
-            forumManager.getGroupCount(txn, groupItem.id)
-        }
-        val timestamp = max(count.latestMsgTime + 1, clock.currentTimeMillis())
-        val post = withContext(cryptoDispatcher) {
-            forumManager.createLocalPost(groupItem.id, text, timestamp, parentId, author)
-        }
-        runOnDbThreadWithTransaction(false) { txn ->
-            val header = forumManager.addLocalPost(txn, post)
-            txn.attach {
-                val item = ForumPostItem(header, text)
-                addItem(item, item.id)
-            }
-        }
-    }
-
-    @UiExecutor
-    private fun addItem(item: ForumPostItem, scrollTo: MessageId? = null) {
-        // If items haven't loaded, we need to wait until they have.
-        // Since this was a R/W DB transaction, the load will pick up this item.
-        val tree = (posts.value as? Loaded)?.messageTree ?: return
-        tree.add(item)
-        _posts.value = Loaded(tree, scrollTo)
-    }
-
-    fun deleteGroup(groupItem: GroupItem) {
-        forumManager.removeForum((groupItem as ForumItem).forum)
-    }
 }
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupConversationScreen.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupConversationScreen.kt
index 6540ceec2f0998784cc40f562a68a5620379fcd9..1eb22c902051067fd90df8c7943bfa6934aaaa14 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupConversationScreen.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupConversationScreen.kt
@@ -41,6 +41,7 @@ import androidx.compose.material.Text
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.MoreVert
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment.Companion.BottomCenter
@@ -49,7 +50,6 @@ import androidx.compose.ui.Alignment.Companion.CenterVertically
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.text.style.TextOverflow.Companion.Ellipsis
 import androidx.compose.ui.unit.dp
-import org.briarproject.bramble.api.sync.MessageId
 import org.briarproject.briar.desktop.contact.ContactDropDown.State.CLOSED
 import org.briarproject.briar.desktop.contact.ContactDropDown.State.MAIN
 import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE
@@ -60,9 +60,11 @@ import org.briarproject.briar.desktop.viewmodel.viewModel
 @Composable
 fun GroupConversationScreen(
     groupItem: GroupItem,
-    viewModel: ForumViewModel = viewModel(),
+    viewModel: ThreadedConversationViewModel = viewModel(),
 ) {
-    val selectedPost = remember { mutableStateOf<MessageId?>(null) }
+    LaunchedEffect(groupItem) {
+        viewModel.setGroupItem(groupItem)
+    }
     Scaffold(
         topBar = {
             GroupConversationHeader(groupItem) { viewModel.deleteGroup(groupItem) }
@@ -70,13 +72,14 @@ fun GroupConversationScreen(
         content = { padding ->
             ThreadedConversationScreen(
                 postsState = viewModel.posts.value,
-                selectedPost = selectedPost,
+                selectedPost = viewModel.selectedPost.value,
+                onPostSelected = viewModel::selectPost,
                 modifier = Modifier.padding(padding)
             )
         },
         bottomBar = {
-            GroupInputComposable(selectedPost) { text ->
-                viewModel.createPost(groupItem, text, selectedPost.value)
+            GroupInputComposable(viewModel.selectedPost.value) { text ->
+                viewModel.createPost(groupItem, text, viewModel.selectedPost.value?.id)
             }
         }
     )
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupInputComposable.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupInputComposable.kt
index 1b1b7120e949613b14de6922a9b1db78546baec6..1543d36ce979685a9bb2a02cf206f3e3ead42b38 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupInputComposable.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupInputComposable.kt
@@ -32,7 +32,6 @@ import androidx.compose.material.TextFieldExt.moveFocusOnTab
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Send
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.State
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.ui.ExperimentalComposeUiApi
@@ -40,7 +39,6 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.input.pointer.PointerIconDefaults
 import androidx.compose.ui.input.pointer.pointerHoverIcon
 import androidx.compose.ui.unit.dp
-import org.briarproject.bramble.api.sync.MessageId
 import org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_POST_TEXT_LENGTH
 import org.briarproject.briar.desktop.theme.sendButton
 import org.briarproject.briar.desktop.ui.HorizontalDivider
@@ -49,7 +47,7 @@ import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
 @Composable
 @OptIn(ExperimentalComposeUiApi::class)
 fun GroupInputComposable(
-    selectedPost: State<MessageId?>,
+    selectedPost: ThreadItem?,
     onSend: (String) -> Unit,
 ) {
     val postText = rememberSaveable { mutableStateOf("") }
@@ -70,7 +68,7 @@ fun GroupInputComposable(
             textStyle = MaterialTheme.typography.body1,
             placeholder = {
                 Text(
-                    text = if (selectedPost.value == null) {
+                    text = if (selectedPost == null) {
                         i18n("forum.message.hint")
                     } else {
                         i18n("forum.message.reply.hint")
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupListComposable.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupListComposable.kt
index 7ced0abe86fdff5e0996e1ba559ed226b21d7f73..0c08e2a6d5cb208c064bb2ee6909c6dac4fef5aa 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupListComposable.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupListComposable.kt
@@ -36,7 +36,6 @@ import androidx.compose.material.Surface
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.AddComment
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.State
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.semantics.contentDescription
@@ -52,9 +51,9 @@ import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
 
 @Composable
 fun GroupListComposable(
-    list: State<List<GroupItem>>,
+    list: List<GroupItem>,
     isSelected: (GroupId) -> Boolean,
-    filterBy: State<String>,
+    filterBy: String,
     onFilterSet: (String) -> Unit,
     onGroupItemSelected: (GroupItem) -> Unit,
     onAddButtonClicked: () -> Unit,
@@ -71,7 +70,7 @@ fun GroupListComposable(
                 SearchTextField(
                     placeholder = i18n("forum.search.title"),
                     icon = Icons.Filled.AddComment,
-                    searchValue = filterBy.value,
+                    searchValue = filterBy,
                     addButtonDescription = i18n("forum.add.title"),
                     onValueChange = onFilterSet,
                     onAddButtonClicked = onAddButtonClicked,
@@ -85,7 +84,7 @@ fun GroupListComposable(
                             contentDescription = i18n("access.forums.list")
                         }.selectableGroup()
                 ) {
-                    items(list.value) { item ->
+                    items(list) { item ->
                         GroupCard(
                             item = item,
                             onGroupItemSelected = onGroupItemSelected,
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ThreadedConversationScreen.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ThreadedConversationScreen.kt
index 679c9492f83d6f47d3126fc493ab144ae82e4a08..7f899a9e7d5a9b44cd3c9867af07f98264ab973b 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ThreadedConversationScreen.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ThreadedConversationScreen.kt
@@ -34,13 +34,11 @@ import androidx.compose.foundation.selection.selectableGroup
 import androidx.compose.material.MaterialTheme
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Alignment.Companion.CenterEnd
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
 import kotlinx.coroutines.launch
-import org.briarproject.bramble.api.sync.MessageId
 import org.briarproject.briar.desktop.forums.ThreadItem.Companion.UNDEFINED
 import org.briarproject.briar.desktop.theme.selectedCard
 import org.briarproject.briar.desktop.theme.surfaceVariant
@@ -50,8 +48,9 @@ import org.briarproject.briar.desktop.ui.Loader
 @Composable
 fun ThreadedConversationScreen(
     postsState: PostsState,
-    selectedPost: MutableState<MessageId?>,
-    modifier: Modifier = Modifier
+    selectedPost: ThreadItem?,
+    onPostSelected: (ThreadItem) -> Unit,
+    modifier: Modifier = Modifier,
 ) = when (postsState) {
     Loading -> Loader()
     is Loaded -> {
@@ -68,7 +67,7 @@ fun ThreadedConversationScreen(
                 modifier = Modifier.selectableGroup()
             ) {
                 items(postsState.posts) { item ->
-                    ThreadItemComposable(item, selectedPost)
+                    ThreadItemComposable(item, selectedPost, onPostSelected)
                     HorizontalDivider()
                 }
             }
@@ -81,8 +80,12 @@ fun ThreadedConversationScreen(
 }
 
 @Composable
-fun ThreadItemComposable(item: ThreadItem, selectedPost: MutableState<MessageId?>) {
-    val isSelected = selectedPost.value == item.id
+fun ThreadItemComposable(
+    item: ThreadItem,
+    selectedPost: ThreadItem?,
+    onPostSelected: (ThreadItem) -> Unit,
+) {
+    val isSelected = selectedPost == item
     Text(
         text = item.text,
         modifier = Modifier
@@ -95,7 +98,7 @@ fun ThreadItemComposable(item: ThreadItem, selectedPost: MutableState<MessageId?
                 }
             ).selectable(
                 selected = isSelected,
-                onClick = { selectedPost.value = item.id }
+                onClick = { onPostSelected(item) }
             )
             .padding(4.dp)
             .padding(
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ThreadedConversationViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ThreadedConversationViewModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..bea6fc19a3b9cb9c14b23952b519684b333b8cc6
--- /dev/null
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ThreadedConversationViewModel.kt
@@ -0,0 +1,141 @@
+/*
+ * Briar Desktop
+ * Copyright (C) 2021-2022 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.forums
+
+import androidx.compose.runtime.mutableStateOf
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.briarproject.bramble.api.crypto.CryptoExecutor
+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.identity.IdentityManager
+import org.briarproject.bramble.api.identity.LocalAuthor
+import org.briarproject.bramble.api.lifecycle.LifecycleManager
+import org.briarproject.bramble.api.sync.GroupId
+import org.briarproject.bramble.api.sync.MessageId
+import org.briarproject.bramble.api.system.Clock
+import org.briarproject.briar.api.client.MessageTracker
+import org.briarproject.briar.api.forum.ForumManager
+import org.briarproject.briar.client.MessageTreeImpl
+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
+import org.slf4j.LoggerFactory.getLogger
+import java.lang.Long.max
+import javax.inject.Inject
+
+sealed class PostsState
+object Loading : PostsState()
+class Loaded(
+    val messageTree: MessageTreeImpl<ThreadItem>,
+    val scrollTo: MessageId? = null,
+) : PostsState() {
+    val posts: MutableList<ThreadItem> get() = messageTree.depthFirstOrder()
+}
+
+class ThreadedConversationViewModel @Inject constructor(
+    private val forumManager: ForumManager,
+    private val identityManager: IdentityManager,
+    private val clock: Clock,
+    @CryptoExecutor private val cryptoDispatcher: CoroutineDispatcher,
+    briarExecutors: BriarExecutors,
+    lifecycleManager: LifecycleManager,
+    db: TransactionManager,
+    eventBus: EventBus,
+) : EventListenerDbViewModel(briarExecutors, lifecycleManager, db, eventBus) {
+
+    companion object {
+        private val LOG = getLogger(ThreadedConversationViewModel::class.java)
+    }
+
+    private lateinit var groupItem: GroupItem
+
+    private val _posts = mutableStateOf<PostsState>(Loading)
+    val posts = _posts.asState()
+
+    private val _selectedPost = mutableStateOf<ThreadItem?>(null)
+    val selectedPost = _selectedPost.asState()
+
+    @UiExecutor
+    fun setGroupItem(groupItem: GroupItem) {
+        this.groupItem = groupItem
+        loadPosts(groupItem.id)
+    }
+
+    override fun eventOccurred(e: Event) {
+    }
+
+    private fun loadPosts(groupId: GroupId) {
+        _posts.value = Loading
+        runOnDbThreadWithTransaction(true) { txn ->
+            val items = forumManager.getPostHeaders(txn, groupId).map { header ->
+                ForumPostItem(header, forumManager.getPostText(txn, header.id))
+            }
+            val tree = MessageTreeImpl<ThreadItem>().apply { add(items) }
+            txn.attach {
+                _posts.value = Loaded(tree)
+            }
+        }
+    }
+
+    @UiExecutor
+    fun selectPost(post: ThreadItem?) {
+        _selectedPost.value = post
+    }
+
+    @UiExecutor
+    @OptIn(DelicateCoroutinesApi::class)
+    fun createPost(groupItem: GroupItem, text: String, parentId: MessageId?) = GlobalScope.launch {
+        val author = runOnDbThreadWithTransaction<LocalAuthor>(false) { txn ->
+            identityManager.getLocalAuthor(txn)
+        }
+        val count = runOnDbThreadWithTransaction<MessageTracker.GroupCount>(false) { txn ->
+            forumManager.getGroupCount(txn, groupItem.id)
+        }
+        val timestamp = max(count.latestMsgTime + 1, clock.currentTimeMillis())
+        val post = withContext(cryptoDispatcher) {
+            forumManager.createLocalPost(groupItem.id, text, timestamp, parentId, author)
+        }
+        runOnDbThreadWithTransaction(false) { txn ->
+            val header = forumManager.addLocalPost(txn, post)
+            txn.attach {
+                val item = ForumPostItem(header, text)
+                addItem(item, item.id)
+            }
+        }
+    }
+
+    @UiExecutor
+    private fun addItem(item: ForumPostItem, scrollTo: MessageId? = null) {
+        // If items haven't loaded, we need to wait until they have.
+        // Since this was a R/W DB transaction, the load will pick up this item.
+        val tree = (posts.value as? Loaded)?.messageTree ?: return
+        tree.add(item)
+        _posts.value = Loaded(tree, scrollTo)
+    }
+
+    fun deleteGroup(groupItem: GroupItem) {
+        forumManager.removeForum((groupItem as ForumItem).forum)
+    }
+}
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroups/ThreadedConversationScreen.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroups/ThreadedConversationScreen.kt
index f5cad0ee065cb005f0af24fce2fd7fab158f642a..3708d9ad8e52c39440f2f6148b9b8a68fbeaa430 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroups/ThreadedConversationScreen.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroups/ThreadedConversationScreen.kt
@@ -21,10 +21,8 @@ package org.briarproject.briar.desktop.privategroups
 import androidx.compose.runtime.Composable
 import org.briarproject.bramble.api.sync.GroupId
 import org.briarproject.briar.desktop.ui.UiPlaceholder
-import org.briarproject.briar.desktop.viewmodel.viewModel
 
 @Composable
 fun ThreadedConversationScreen(
     groupId: GroupId,
-    viewModel: ThreadedConversationViewModel = viewModel(),
 ) = UiPlaceholder()
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 bcb43539967d35b874c42033ecf5b643927ed529..45797f9d8fa321854e7ce730e39788cccdf0dc85 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
@@ -26,11 +26,11 @@ import org.briarproject.briar.desktop.contact.ContactListViewModel
 import org.briarproject.briar.desktop.contact.add.remote.AddContactViewModel
 import org.briarproject.briar.desktop.conversation.ConversationViewModel
 import org.briarproject.briar.desktop.forums.ForumViewModel
+import org.briarproject.briar.desktop.forums.ThreadedConversationViewModel
 import org.briarproject.briar.desktop.introduction.IntroductionViewModel
 import org.briarproject.briar.desktop.login.StartupViewModel
 import org.briarproject.briar.desktop.navigation.SidebarViewModel
 import org.briarproject.briar.desktop.privategroups.PrivateGroupListViewModel
-import org.briarproject.briar.desktop.privategroups.ThreadedConversationViewModel
 import org.briarproject.briar.desktop.settings.SettingsViewModel
 import kotlin.reflect.KClass