diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumItem.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumItem.kt
index 266c7bc7c88bbbfaff0175f59a3d992802640e85..22c555b23f48b6a3d947ad48bc4fda10c1300ef5 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumItem.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumItem.kt
@@ -1,6 +1,6 @@
 /*
  * Briar Desktop
- * Copyright (C) 2021-2022 The Briar Project
+ * 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
@@ -20,18 +20,11 @@ package org.briarproject.briar.desktop.forums
 
 import org.briarproject.bramble.api.sync.GroupId
 import org.briarproject.briar.api.client.MessageTracker
+import org.briarproject.briar.api.client.PostHeader
 import org.briarproject.briar.api.forum.Forum
-import org.briarproject.briar.api.forum.ForumPostHeader
+import org.briarproject.briar.desktop.group.GroupItem
 import kotlin.math.max
 
-interface GroupItem {
-    val id: GroupId
-    val name: String
-    val msgCount: Int
-    val unread: Int
-    val timestamp: Long
-}
-
 data class ForumItem(
     val forum: Forum,
     override val msgCount: Int,
@@ -46,10 +39,10 @@ data class ForumItem(
         timestamp = groupCount.latestMsgTime,
     )
 
-    override val id: GroupId get() = forum.id
-    override val name: String get() = forum.name
+    override val id: GroupId = forum.id
+    override val name: String = forum.name
 
-    fun updateOnPostReceived(header: ForumPostHeader) =
+    fun updateOnPostReceived(header: PostHeader) =
         copy(
             msgCount = msgCount + 1,
             unread = if (header.isRead) unread else unread + 1,
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumListViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumListViewModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6fbb5d74cea942fb5c591fe3589aadb4246419d1
--- /dev/null
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumListViewModel.kt
@@ -0,0 +1,96 @@
+/*
+ * 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.forums
+
+import androidx.compose.runtime.mutableStateListOf
+import mu.KotlinLogging
+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.sync.ClientId
+import org.briarproject.bramble.api.sync.GroupId
+import org.briarproject.briar.api.client.PostHeader
+import org.briarproject.briar.api.forum.ForumManager
+import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent
+import org.briarproject.briar.desktop.group.GroupListViewModel
+import org.briarproject.briar.desktop.threading.BriarExecutors
+import org.briarproject.briar.desktop.utils.removeFirst
+import org.briarproject.briar.desktop.utils.replaceFirst
+import javax.inject.Inject
+
+class ForumListViewModel
+@Inject constructor(
+    private val forumManager: ForumManager,
+    threadViewModel: ThreadedConversationViewModel,
+    briarExecutors: BriarExecutors,
+    lifecycleManager: LifecycleManager,
+    db: TransactionManager,
+    eventBus: EventBus,
+) : GroupListViewModel<ForumItem>(threadViewModel, briarExecutors, lifecycleManager, db, eventBus) {
+
+    companion object {
+        private val LOG = KotlinLogging.logger {}
+    }
+
+    override val clientId: ClientId = ForumManager.CLIENT_ID
+
+    override val _groupList = mutableStateListOf<ForumItem>()
+
+    override fun eventOccurred(e: Event) {
+        super.eventOccurred(e)
+        when (e) {
+            is ForumPostReceivedEvent -> {
+                updateItem(e.groupId) { it.updateOnPostReceived(e.header) }
+            }
+
+            is ForumPostReadEvent -> {
+                updateItem(e.groupId) { it.updateOnPostsRead(e.numMarkedRead) }
+            }
+        }
+    }
+
+    override fun createGroupItem(txn: Transaction, id: GroupId) = ForumItem(
+        forum = forumManager.getForum(txn, id),
+        groupCount = forumManager.getGroupCount(txn, id),
+    )
+
+    fun createForum(name: String) = runOnDbThread {
+        forumManager.addForum(name)
+    }
+
+    override fun loadGroups(txn: Transaction) =
+        forumManager.getForums(txn).map { forums ->
+            ForumItem(
+                forum = forums,
+                groupCount = forumManager.getGroupCount(txn, forums.id),
+            )
+        }
+
+    override fun addOwnMessage(header: PostHeader) {
+        selectedGroupId.value?.let { id -> updateItem(id) { it.updateOnPostReceived(header) } }
+    }
+
+    private fun updateItem(groupId: GroupId, update: (ForumItem) -> ForumItem) =
+        _groupList.replaceFirst({ it.id == groupId }, update)
+
+    override fun removeItem(groupId: GroupId) =
+        _groupList.removeFirst { it.id == groupId }
+}
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 c815b1e7e4c7563d3f955aa085a58c300628ad63..0305332497297e30fefec9ee3d143b11119f9c74 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
@@ -1,6 +1,6 @@
 /*
  * Briar Desktop
- * Copyright (C) 2021-2022 The Briar Project
+ * 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
@@ -18,76 +18,25 @@
 
 package org.briarproject.briar.desktop.forums
 
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.AddComment
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import org.briarproject.briar.desktop.conversation.Explainer
-import org.briarproject.briar.desktop.ui.ColoredIconButton
-import org.briarproject.briar.desktop.ui.VerticalDivider
-import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
+import org.briarproject.briar.desktop.group.GroupScreen
 import org.briarproject.briar.desktop.viewmodel.viewModel
 
 @Composable
 fun ForumScreen(
-    viewModel: ForumViewModel = viewModel(),
-) {
-    val addDialogVisible = remember { mutableStateOf(false) }
-    AddForumDialog(
-        visible = addDialogVisible.value,
-        onCreate = { name ->
-            viewModel.createForum(name)
-            addDialogVisible.value = false
-        },
-        onCancelButtonClicked = { addDialogVisible.value = false }
-    )
-
-    if (viewModel.noForumsYet.value) {
-        NoForumsYet { addDialogVisible.value = true }
-    } else {
-        Row(modifier = Modifier.fillMaxWidth()) {
-            GroupList(
-                list = viewModel.forumList.value,
-                isSelected = viewModel::isSelected,
-                filterBy = viewModel.filterBy.value,
-                onFilterSet = viewModel::setFilterBy,
-                onGroupItemSelected = viewModel::selectGroup,
-                onAddButtonClicked = { addDialogVisible.value = true },
-            )
-            VerticalDivider()
-            Column(modifier = Modifier.weight(1f).fillMaxHeight()) {
-                if (viewModel.selectedGroupId.value == null) {
-                    NoForumSelected()
-                } else {
-                    GroupConversationScreen(viewModel.threadViewModel)
-                }
-            }
-        }
-    }
-}
-
-@Composable
-fun NoForumsYet(onContactAdd: () -> Unit) = Explainer(
-    headline = i18n("welcome.title"),
-    text = i18n("forum.empty_state.text"),
-) {
-    ColoredIconButton(
-        icon = Icons.Filled.AddComment,
-        iconSize = 20.dp,
-        contentDescription = i18n("access.forums.add"),
-        onClick = onContactAdd,
-    )
-}
-
-@Composable
-fun NoForumSelected() = Explainer(
-    headline = i18n("forum.none_selected.title"),
-    text = i18n("forum.none_selected.hint"),
+    viewModel: ForumListViewModel = viewModel(),
+) = GroupScreen(
+    strings = ForumStrings,
+    viewModel = viewModel,
+    addGroupDialog = { visible ->
+        AddForumDialog(
+            visible = visible.value,
+            onCreate = { name ->
+                viewModel.createForum(name)
+                visible.value = false
+            },
+            onCancelButtonClicked = { visible.value = false }
+        )
+    },
+    conversationScreen = { GroupConversationScreen(viewModel.threadViewModel) }
 )
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumStrings.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumStrings.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ceee63746d7066c186866c2a34601fe7e253c45e
--- /dev/null
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumStrings.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.forums
+
+import org.briarproject.briar.desktop.group.GroupStrings
+import org.briarproject.briar.desktop.utils.InternationalizationUtils
+import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
+import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18nF
+import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18nP
+import org.briarproject.briar.desktop.utils.TimeUtils
+
+object ForumStrings : GroupStrings(
+    listTitle = i18n("forum.search.title"),
+    listDescription = i18n("access.forums.list"),
+    addButtonDescription = i18n("forum.add.title"),
+    noGroupsYet = i18n("forum.empty_state.text"),
+    noGroupSelectedTitle = i18n("forum.none_selected.title"),
+    noGroupSelectedText = i18n("forum.none_selected.hint"),
+    messageCount = { count ->
+        if (count > 0) i18nP("group.card.posts", count)
+        else i18n("group.card.no_posts")
+    },
+    unreadCount = { count ->
+        i18nP("access.forums.unread_count", count)
+    },
+    lastMessage = { timestamp ->
+        i18nF("access.forums.last_post_timestamp", timestamp)
+    },
+)
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 11fafd1e389e7e15002aad6980d033ba1d24f0b1..5392282bc61eff32dc83960d2e4d169f7d962139 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
@@ -1,6 +1,6 @@
 /*
  * Briar Desktop
- * Copyright (C) 2021-2022 The Briar Project
+ * 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
@@ -56,6 +56,9 @@ import org.briarproject.briar.desktop.contact.ContactDropDown.State.MAIN
 import org.briarproject.briar.desktop.forums.sharing.ForumSharingActionDrawerContent
 import org.briarproject.briar.desktop.forums.sharing.ForumSharingStatusDrawerContent
 import org.briarproject.briar.desktop.forums.sharing.ForumSharingViewModel
+import org.briarproject.briar.desktop.group.GroupCircle
+import org.briarproject.briar.desktop.group.GroupInputComposable
+import org.briarproject.briar.desktop.group.GroupItem
 import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE
 import org.briarproject.briar.desktop.ui.HorizontalDivider
 import org.briarproject.briar.desktop.ui.getInfoDrawerHandler
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
index b1561a4adca2e3009829109591f8be8ad904dbf7..ba6c0cea348af62d2c990a3c32640665a76eb784 100644
--- 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
@@ -1,6 +1,6 @@
 /*
  * Briar Desktop
- * Copyright (C) 2021-2022 The Briar Project
+ * 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
@@ -41,6 +41,7 @@ import org.briarproject.briar.api.forum.ForumPostHeader
 import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent
 import org.briarproject.briar.client.MessageTreeImpl
 import org.briarproject.briar.desktop.forums.sharing.ForumSharingViewModel
+import org.briarproject.briar.desktop.group.GroupItem
 import org.briarproject.briar.desktop.threading.BriarExecutors
 import org.briarproject.briar.desktop.threading.UiExecutor
 import org.briarproject.briar.desktop.viewmodel.EventListenerDbViewModel
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupCircle.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/GroupCircle.kt
similarity index 96%
rename from briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupCircle.kt
rename to briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/GroupCircle.kt
index 1a41ac1f4dd732c1a4cabde33c2943d0de611b6e..599dd6770f07a8a5b5cba35ca0d271b835896642 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupCircle.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/GroupCircle.kt
@@ -1,6 +1,6 @@
 /*
  * Briar Desktop
- * Copyright (C) 2021-2022 The Briar Project
+ * 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
@@ -16,7 +16,7 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-package org.briarproject.briar.desktop.forums
+package org.briarproject.briar.desktop.group
 
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
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/group/GroupInputComposable.kt
similarity index 96%
rename from briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupInputComposable.kt
rename to briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/GroupInputComposable.kt
index 09225a8e2fd347669d74c2e47d2b4d467ffd5c8d..9d096aadfd5d9bc2ee363c8ade9533263c4d5341 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupInputComposable.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/GroupInputComposable.kt
@@ -1,6 +1,6 @@
 /*
  * Briar Desktop
- * Copyright (C) 2021-2022 The Briar Project
+ * 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
@@ -16,7 +16,7 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-package org.briarproject.briar.desktop.forums
+package org.briarproject.briar.desktop.group
 
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
@@ -45,6 +45,8 @@ import androidx.compose.ui.input.pointer.PointerIconDefaults
 import androidx.compose.ui.input.pointer.pointerHoverIcon
 import androidx.compose.ui.unit.dp
 import org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_POST_TEXT_LENGTH
+import org.briarproject.briar.desktop.forums.ThreadItem
+import org.briarproject.briar.desktop.forums.ThreadItemContentComposable
 import org.briarproject.briar.desktop.theme.divider
 import org.briarproject.briar.desktop.theme.sendButton
 import org.briarproject.briar.desktop.ui.HorizontalDivider
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/group/GroupItem.kt
similarity index 72%
rename from briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroups/ThreadedConversationScreen.kt
rename to briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/GroupItem.kt
index 3708d9ad8e52c39440f2f6148b9b8a68fbeaa430..588cc76afef22212010a6a4a64bb4ec414fcad00 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroups/ThreadedConversationScreen.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/GroupItem.kt
@@ -1,6 +1,6 @@
 /*
  * Briar Desktop
- * Copyright (C) 2021-2022 The Briar Project
+ * 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
@@ -16,13 +16,14 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-package org.briarproject.briar.desktop.privategroups
+package org.briarproject.briar.desktop.group
 
-import androidx.compose.runtime.Composable
 import org.briarproject.bramble.api.sync.GroupId
-import org.briarproject.briar.desktop.ui.UiPlaceholder
 
-@Composable
-fun ThreadedConversationScreen(
-    groupId: GroupId,
-) = UiPlaceholder()
+interface GroupItem {
+    val id: GroupId
+    val name: String
+    val msgCount: Int
+    val unread: Int
+    val timestamp: Long
+}
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupItemView.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/GroupItemView.kt
similarity index 80%
rename from briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupItemView.kt
rename to briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/GroupItemView.kt
index 49d380505f74b4ec9851a85c9cad26a81a61a791..fff90eb904ab27bc73aebb68f380cad08e2ad205 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupItemView.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/GroupItemView.kt
@@ -1,6 +1,6 @@
 /*
  * Briar Desktop
- * Copyright (C) 2021-2022 The Briar Project
+ * 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
@@ -16,7 +16,7 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-package org.briarproject.briar.desktop.forums
+package org.briarproject.briar.desktop.group
 
 import androidx.compose.foundation.layout.Arrangement.SpaceBetween
 import androidx.compose.foundation.layout.Arrangement.spacedBy
@@ -43,10 +43,8 @@ import androidx.compose.ui.semantics.text
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import org.briarproject.bramble.api.sync.GroupId
+import org.briarproject.briar.desktop.forums.ForumStrings
 import org.briarproject.briar.desktop.ui.NumberBadge
-import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
-import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18nF
-import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18nP
 import org.briarproject.briar.desktop.utils.PreviewUtils.preview
 import org.briarproject.briar.desktop.utils.TimeUtils.getFormattedTimestamp
 import org.briarproject.briar.desktop.utils.appendCommaSeparated
@@ -67,11 +65,12 @@ fun main() = preview(
         override val unread: Int = getIntParameter("unread")
         override val timestamp: Long = getLongParameter("timestamp")
     }
-    GroupItemView(item)
+    GroupItemView(ForumStrings, item)
 }
 
 @Composable
 fun GroupItemView(
+    strings: GroupStrings,
     groupItem: GroupItem,
     modifier: Modifier = Modifier,
 ) = Row(
@@ -81,7 +80,7 @@ fun GroupItemView(
         // allows content to be bottom-aligned
         .height(IntrinsicSize.Min)
         .semantics {
-            text = getDescription(groupItem)
+            text = getDescription(strings, groupItem)
         },
 ) {
     Box(Modifier.align(Top).padding(vertical = 8.dp)) {
@@ -91,23 +90,18 @@ fun GroupItemView(
             modifier = Modifier.align(TopEnd).offset(6.dp, (-6).dp)
         )
     }
-    GroupItemViewInfo(groupItem)
+    GroupItemViewInfo(strings, groupItem)
 }
 
-private fun getDescription(item: GroupItem) = buildBlankAnnotatedString {
+private fun getDescription(strings: GroupStrings, item: GroupItem) = buildBlankAnnotatedString {
     append(item.name)
-    if (item.unread > 0) appendCommaSeparated(i18nP("access.forums.unread_count", item.unread))
-    if (item.msgCount == 0) appendCommaSeparated(i18n("group.card.no_posts"))
-    else appendCommaSeparated(
-        i18nF(
-            "access.forums.last_post_timestamp",
-            getFormattedTimestamp(item.timestamp)
-        )
-    )
+    if (item.unread > 0) appendCommaSeparated(strings.unreadCount(item.unread))
+    appendCommaSeparated(strings.messageCount(item.msgCount))
+    if (item.msgCount > 0) appendCommaSeparated(strings.lastMessage(getFormattedTimestamp(item.timestamp)))
 }
 
 @Composable
-private fun GroupItemViewInfo(groupItem: GroupItem) = Column(
+private fun GroupItemViewInfo(strings: GroupStrings, groupItem: GroupItem) = Column(
     horizontalAlignment = Start,
 ) {
     Spacer(Modifier.weight(1f, fill = true))
@@ -123,8 +117,7 @@ private fun GroupItemViewInfo(groupItem: GroupItem) = Column(
         modifier = Modifier.fillMaxWidth()
     ) {
         Text(
-            text = if (groupItem.msgCount > 0) i18nP("group.card.posts", groupItem.msgCount)
-            else i18nP("group.card.no_posts", groupItem.msgCount),
+            text = strings.messageCount(groupItem.msgCount),
             style = MaterialTheme.typography.caption
         )
         if (groupItem.msgCount > 0) {
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupList.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/GroupList.kt
similarity index 93%
rename from briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupList.kt
rename to briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/GroupList.kt
index ddfd369bd432b6bc79b8b37dffd8c215fe34a4a1..30ab73ee5a83692acab86095c6da74062c3663d9 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupList.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/GroupList.kt
@@ -1,6 +1,6 @@
 /*
  * Briar Desktop
- * Copyright (C) 2021-2022 The Briar Project
+ * 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
@@ -16,7 +16,7 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-package org.briarproject.briar.desktop.forums
+package org.briarproject.briar.desktop.group
 
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Column
@@ -41,13 +41,13 @@ import androidx.compose.ui.semantics.contentDescription
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.unit.dp
 import org.briarproject.bramble.api.sync.GroupId
+import org.briarproject.briar.desktop.forums.ForumStrings
 import org.briarproject.briar.desktop.theme.surfaceVariant
 import org.briarproject.briar.desktop.ui.Constants.COLUMN_WIDTH
 import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE
 import org.briarproject.briar.desktop.ui.ListItemView
 import org.briarproject.briar.desktop.ui.SearchTextField
 import org.briarproject.briar.desktop.ui.VerticallyScrollableArea
-import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
 import org.briarproject.briar.desktop.utils.PreviewUtils.preview
 import java.time.Instant
 
@@ -90,6 +90,7 @@ fun main() = preview {
     }
 
     GroupList(
+        strings = ForumStrings,
         list = filteredList,
         isSelected = { selected?.id == it },
         filterBy = filterBy,
@@ -101,6 +102,7 @@ fun main() = preview {
 
 @Composable
 fun GroupList(
+    strings: GroupStrings,
     list: List<GroupItem>,
     isSelected: (GroupId) -> Boolean,
     filterBy: String,
@@ -115,10 +117,10 @@ fun GroupList(
         modifier = Modifier.fillMaxWidth().height(HEADER_SIZE + 1.dp),
     ) {
         SearchTextField(
-            placeholder = i18n("forum.search.title"),
+            placeholder = strings.listTitle,
             icon = Icons.Filled.AddComment,
             searchValue = filterBy,
-            addButtonDescription = i18n("forum.add.title"),
+            addButtonDescription = strings.addButtonDescription,
             onValueChange = onFilterSet,
             onAddButtonClicked = onAddButtonClicked,
         )
@@ -128,7 +130,7 @@ fun GroupList(
             state = scrollState,
             modifier = Modifier
                 .semantics {
-                    contentDescription = i18n("access.forums.list")
+                    contentDescription = strings.listDescription
                 }.selectableGroup()
         ) {
             items(
@@ -142,6 +144,7 @@ fun GroupList(
                     dividerOffsetFromStart = (16 + 36 + 12).dp,
                 ) {
                     GroupItemView(
+                        strings = strings,
                         groupItem = item,
                         modifier = Modifier
                             .heightIn(min = HEADER_SIZE)
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/group/GroupListViewModel.kt
similarity index 60%
rename from briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumViewModel.kt
rename to briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/GroupListViewModel.kt
index 507d80a609dffa08a3cfe1bc43d8638b43cea030..225da54ce6044c4a68d8f400e5843f4579a1447a 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumViewModel.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/GroupListViewModel.kt
@@ -1,6 +1,6 @@
 /*
  * Briar Desktop
- * Copyright (C) 2021-2022 The Briar Project
+ * 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
@@ -16,33 +16,30 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-package org.briarproject.briar.desktop.forums
+package org.briarproject.briar.desktop.group
 
 import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.mutableStateListOf
 import androidx.compose.runtime.mutableStateOf
 import mu.KotlinLogging
+import org.briarproject.bramble.api.db.DatabaseExecutor
+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.sync.ClientId
 import org.briarproject.bramble.api.sync.GroupId
 import org.briarproject.bramble.api.sync.event.GroupAddedEvent
 import org.briarproject.bramble.api.sync.event.GroupRemovedEvent
-import org.briarproject.briar.api.forum.ForumManager
-import org.briarproject.briar.api.forum.ForumPostHeader
-import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent
+import org.briarproject.briar.api.client.PostHeader
+import org.briarproject.briar.desktop.forums.ThreadedConversationViewModel
 import org.briarproject.briar.desktop.threading.BriarExecutors
 import org.briarproject.briar.desktop.utils.clearAndAddAll
-import org.briarproject.briar.desktop.utils.removeFirst
-import org.briarproject.briar.desktop.utils.replaceFirst
 import org.briarproject.briar.desktop.viewmodel.EventListenerDbViewModel
 import org.briarproject.briar.desktop.viewmodel.asState
-import javax.inject.Inject
 
-class ForumViewModel @Inject constructor(
+abstract class GroupListViewModel<T : GroupItem>(
     val threadViewModel: ThreadedConversationViewModel,
-    private val forumManager: ForumManager,
     briarExecutors: BriarExecutors,
     lifecycleManager: LifecycleManager,
     db: TransactionManager,
@@ -53,21 +50,23 @@ class ForumViewModel @Inject constructor(
         private val LOG = KotlinLogging.logger {}
     }
 
-    private val _fullForumList = mutableStateListOf<ForumItem>()
-    val forumList = derivedStateOf {
+    protected abstract val clientId: ClientId
+
+    protected abstract val _groupList: MutableList<T> //todo: check internal
+    val list = derivedStateOf {
         val filter = _filterBy.value
-        _fullForumList.filter { item ->
+        _groupList.filter { item ->
             item.name.contains(filter, ignoreCase = true)
         }.sortedByDescending { it.timestamp }
     }
 
-    val noForumsYet = derivedStateOf { _fullForumList.isEmpty() }
+    val noGroupsYet = derivedStateOf { _groupList.isEmpty() }
 
     private val _selectedGroupId = mutableStateOf<GroupId?>(null)
     val selectedGroupId = derivedStateOf {
         // reset selected group item to null if not part of list after filtering
         val groupId = _selectedGroupId.value
-        if (groupId == null || forumList.value.any { it.id == groupId }) {
+        if (groupId == null || list.value.any { it.id == groupId }) {
             groupId
         } else {
             _selectedGroupId.value = null
@@ -94,54 +93,42 @@ class ForumViewModel @Inject constructor(
 
     override fun eventOccurred(e: Event) {
         when {
-            e is GroupAddedEvent && e.group.clientId == ForumManager.CLIENT_ID ->
+            e is GroupAddedEvent && e.group.clientId == clientId ->
                 onGroupAdded(e.group.id)
 
-            e is GroupRemovedEvent && e.group.clientId == ForumManager.CLIENT_ID -> {
+            e is GroupRemovedEvent && e.group.clientId == clientId -> {
                 removeItem(e.group.id)
                 if (_selectedGroupId.value == e.group.id) _selectedGroupId.value = null
             }
-
-            e is ForumPostReceivedEvent -> {
-                updateItem(e.groupId) { it.updateOnPostReceived(e.header) }
-            }
-
-            e is ForumPostReadEvent -> {
-                updateItem(e.groupId) { it.updateOnPostsRead(e.numMarkedRead) }
-            }
         }
     }
 
+    @DatabaseExecutor
+    protected abstract fun createGroupItem(txn: Transaction, id: GroupId): T
+
     private fun onGroupAdded(id: GroupId) = runOnDbThreadWithTransaction(true) { txn ->
-        val item = ForumItem(
-            forum = forumManager.getForum(txn, id),
-            groupCount = forumManager.getGroupCount(txn, id),
-        )
+        val item = createGroupItem(txn, id)
         txn.attach {
             addItem(item)
         }
     }
 
-    fun createForum(name: String) = runOnDbThread {
-        forumManager.addForum(name)
-    }
+    @DatabaseExecutor
+    protected abstract fun loadGroups(txn: Transaction): List<T>
 
     private fun loadGroups() = runOnDbThreadWithTransaction(true) { txn ->
-        val list = forumManager.getForums(txn).map { forums ->
-            ForumItem(
-                forum = forums,
-                groupCount = forumManager.getGroupCount(txn, forums.id),
-            )
-        }
+        val list = loadGroups(txn)
         txn.attach {
-            _fullForumList.clearAndAddAll(list)
+            _groupList.clearAndAddAll(list)
         }
     }
 
+    protected abstract fun addOwnMessage(header: PostHeader)
+
     fun selectGroup(groupItem: GroupItem) {
         if (_selectedGroupId.value == groupItem.id) return
         _selectedGroupId.value = groupItem.id
-        threadViewModel.setGroupItem(groupItem, this::addOwnPost)
+        threadViewModel.setGroupItem(groupItem, this::addOwnMessage)
     }
 
     fun isSelected(groupId: GroupId) = _selectedGroupId.value == groupId
@@ -150,22 +137,7 @@ class ForumViewModel @Inject constructor(
         _filterBy.value = filter
     }
 
-    private fun addOwnPost(header: ForumPostHeader) {
-        selectedGroupId.value?.let { id -> updateItem(id) { it.updateOnPostReceived(header) } }
-    }
-
-    private fun addItem(forumItem: ForumItem) = _fullForumList.add(forumItem)
+    private fun addItem(groupItem: T) = _groupList.add(groupItem)
 
-    private fun updateItem(forumId: GroupId, update: (ForumItem) -> ForumItem) {
-        _fullForumList.replaceFirst(
-            { it.id == forumId },
-            update
-        )
-    }
-
-    private fun removeItem(forumId: GroupId) {
-        _fullForumList.removeFirst {
-            it.id == forumId
-        }
-    }
+    protected abstract fun removeItem(groupId: GroupId): Boolean
 }
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/GroupScreen.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/GroupScreen.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1eb9101a70a20399a3bb7cfb82b4735c3e2c2fb8
--- /dev/null
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/GroupScreen.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.group
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.AddComment
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import org.briarproject.briar.desktop.conversation.Explainer
+import org.briarproject.briar.desktop.ui.ColoredIconButton
+import org.briarproject.briar.desktop.ui.VerticalDivider
+import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
+
+@Composable
+fun <T : GroupItem> GroupScreen(
+    strings: GroupStrings,
+    viewModel: GroupListViewModel<T>,
+    addGroupDialog: @Composable (MutableState<Boolean>) -> Unit,
+    conversationScreen: @Composable () -> Unit,
+) {
+    val addDialogVisible = remember { mutableStateOf(false) }
+    addGroupDialog(addDialogVisible)
+
+    if (viewModel.noGroupsYet.value) {
+        NoGroupsYet(strings) { addDialogVisible.value = true }
+    } else {
+        Row(modifier = Modifier.fillMaxWidth()) {
+            GroupList(
+                strings = strings,
+                list = viewModel.list.value,
+                isSelected = viewModel::isSelected,
+                filterBy = viewModel.filterBy.value,
+                onFilterSet = viewModel::setFilterBy,
+                onGroupItemSelected = viewModel::selectGroup,
+                onAddButtonClicked = { addDialogVisible.value = true },
+            )
+            VerticalDivider()
+            Column(modifier = Modifier.weight(1f).fillMaxHeight()) {
+                if (viewModel.selectedGroupId.value == null) {
+                    NoGroupSelected(strings)
+                } else {
+                    conversationScreen()
+                }
+            }
+        }
+    }
+}
+
+@Composable
+fun NoGroupsYet(strings: GroupStrings, onAdd: () -> Unit) = Explainer(
+    headline = i18n("welcome.title"),
+    text = strings.noGroupsYet,
+) {
+    ColoredIconButton(
+        icon = Icons.Filled.AddComment,
+        iconSize = 20.dp,
+        contentDescription = strings.addButtonDescription,
+        onClick = onAdd,
+    )
+}
+
+@Composable
+fun NoGroupSelected(strings: GroupStrings) = Explainer(
+    headline = strings.noGroupSelectedTitle,
+    text = strings.noGroupSelectedText,
+)
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/GroupStrings.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/GroupStrings.kt
new file mode 100644
index 0000000000000000000000000000000000000000..5ae2f103736e28790ab4bf0fa31b4c88ffdae874
--- /dev/null
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/group/GroupStrings.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.group
+
+abstract class GroupStrings(
+    val listTitle: String,
+    val listDescription: String,
+    val addButtonDescription: String,
+    val noGroupsYet: String,
+    val noGroupSelectedTitle: String,
+    val noGroupSelectedText: String,
+    val messageCount: (Int) -> String,
+    val unreadCount: (Int) -> String,
+    val lastMessage: (String) -> String,
+)
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroups/PrivateGroupItem.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/PrivateGroupItem.kt
similarity index 59%
rename from briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroups/PrivateGroupItem.kt
rename to briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/PrivateGroupItem.kt
index fac142d0d897e4ae95dcff3cdf9cb320e587f10b..2f50b43eb7abdd0bc8cc231f8b7d79dcf63fbe2a 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroups/PrivateGroupItem.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/PrivateGroupItem.kt
@@ -1,6 +1,6 @@
 /*
  * Briar Desktop
- * Copyright (C) 2021-2022 The Briar Project
+ * 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
@@ -16,17 +16,21 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-package org.briarproject.briar.desktop.privategroups
+package org.briarproject.briar.desktop.privategroup
 
+import org.briarproject.bramble.api.sync.GroupId
 import org.briarproject.briar.api.client.MessageTracker
+import org.briarproject.briar.api.client.PostHeader
 import org.briarproject.briar.api.privategroup.PrivateGroup
+import org.briarproject.briar.desktop.group.GroupItem
+import kotlin.math.max
 
 data class PrivateGroupItem(
     val privateGroup: PrivateGroup,
-    val msgCount: Int,
-    val unread: Int,
-    val timestamp: Long
-) {
+    override val msgCount: Int,
+    override val unread: Int,
+    override val timestamp: Long,
+) : GroupItem {
 
     constructor(privateGroup: PrivateGroup, groupCount: MessageTracker.GroupCount) :
         this(
@@ -35,4 +39,17 @@ data class PrivateGroupItem(
             unread = groupCount.unreadCount,
             timestamp = groupCount.latestMsgTime
         )
+
+    override val id: GroupId = privateGroup.id
+    override val name: String = privateGroup.name
+
+    fun updateOnPostReceived(header: PostHeader) =
+        copy(
+            msgCount = msgCount + 1,
+            unread = if (header.isRead) unread else unread + 1,
+            timestamp = max(header.timestamp, this.timestamp)
+        )
+
+    fun updateOnPostsRead(num: Int) =
+        copy(unread = unread - num)
 }
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/PrivateGroupListViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/PrivateGroupListViewModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c273f1c594ec05a3238fb8374dba70c0b081402f
--- /dev/null
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/PrivateGroupListViewModel.kt
@@ -0,0 +1,95 @@
+/*
+ * 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
+
+import androidx.compose.runtime.mutableStateListOf
+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.sync.ClientId
+import org.briarproject.bramble.api.sync.GroupId
+import org.briarproject.briar.api.client.PostHeader
+import org.briarproject.briar.api.forum.ForumManager
+import org.briarproject.briar.api.privategroup.PrivateGroupManager
+import org.briarproject.briar.api.privategroup.event.GroupMessageAddedEvent
+import org.briarproject.briar.desktop.forums.ThreadedConversationViewModel
+import org.briarproject.briar.desktop.group.GroupListViewModel
+import org.briarproject.briar.desktop.threading.BriarExecutors
+import org.briarproject.briar.desktop.utils.removeFirst
+import org.briarproject.briar.desktop.utils.replaceFirst
+import javax.inject.Inject
+
+class PrivateGroupListViewModel
+@Inject constructor(
+    private val privateGroupManager: PrivateGroupManager,
+    threadViewModel: ThreadedConversationViewModel, // todo: subclass
+    briarExecutors: BriarExecutors,
+    lifecycleManager: LifecycleManager,
+    db: TransactionManager,
+    eventBus: EventBus,
+) : GroupListViewModel<PrivateGroupItem>(threadViewModel, briarExecutors, lifecycleManager, db, eventBus) {
+
+    override val clientId: ClientId = ForumManager.CLIENT_ID
+
+    override val _groupList = mutableStateListOf<PrivateGroupItem>()
+
+    override fun eventOccurred(e: Event) {
+        super.eventOccurred(e)
+        when (e) {
+            is GroupMessageAddedEvent -> {
+                updateItem(e.groupId) { it.updateOnPostReceived(e.header) }
+            }
+
+            // TODO
+            /*is ForumPostReadEvent -> {
+                updateItem(e.groupId) { it.updateOnPostsRead(e.numMarkedRead) }
+            }*/
+        }
+    }
+
+    override fun createGroupItem(txn: Transaction, id: GroupId) = PrivateGroupItem(
+        privateGroup = privateGroupManager.getPrivateGroup(txn, id),
+        groupCount = privateGroupManager.getGroupCount(txn, id),
+    )
+
+    fun createPrivateGroup(name: String) = runOnDbThread {
+        TODO()
+        //privateGroupManager.addForum(name)
+    }
+
+    override fun loadGroups(txn: Transaction) =
+        privateGroupManager.getPrivateGroups(txn).map { privateGroup ->
+            PrivateGroupItem(
+                privateGroup = privateGroup,
+                groupCount = privateGroupManager.getGroupCount(txn, privateGroup.id),
+            )
+        }
+
+    override fun addOwnMessage(header: PostHeader) {
+        selectedGroupId.value?.let { id -> updateItem(id) { it.updateOnPostReceived(header) } }
+    }
+
+    private fun updateItem(groupId: GroupId, update: (PrivateGroupItem) -> PrivateGroupItem) =
+        _groupList.replaceFirst({ it.id == groupId }, update)
+
+    override fun removeItem(groupId: GroupId) =
+        _groupList.removeFirst { it.id == groupId }
+}
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroups/PrivateGroupScreen.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/PrivateGroupScreen.kt
similarity index 50%
rename from briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroups/PrivateGroupScreen.kt
rename to briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/PrivateGroupScreen.kt
index 4dde277f0bdd1506defb1822354d2861117c12fe..0f3d5a83875ccf1716ec70d900923686a1e46f8a 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroups/PrivateGroupScreen.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/PrivateGroupScreen.kt
@@ -1,6 +1,6 @@
 /*
  * Briar Desktop
- * Copyright (C) 2021-2022 The Briar Project
+ * 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
@@ -16,36 +16,21 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-package org.briarproject.briar.desktop.privategroups
+package org.briarproject.briar.desktop.privategroup
 
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
+import org.briarproject.briar.desktop.group.GroupScreen
 import org.briarproject.briar.desktop.ui.UiPlaceholder
-import org.briarproject.briar.desktop.ui.VerticalDivider
 import org.briarproject.briar.desktop.viewmodel.viewModel
 
 @Composable
 fun PrivateGroupScreen(
     viewModel: PrivateGroupListViewModel = viewModel(),
-) {
-    Row(modifier = Modifier.fillMaxWidth()) {
-        PrivateGroupList(
-            viewModel.privateGroupList,
-            viewModel::isSelected,
-            viewModel::selectPrivateGroup,
-        )
-        VerticalDivider()
-        Column(modifier = Modifier.weight(1f).fillMaxHeight()) {
-            val id = viewModel.selectedPrivateGroupId.value
-            if (id != null) {
-                ThreadedConversationScreen(id)
-            } else {
-                UiPlaceholder()
-            }
-        }
-    }
-}
+) = GroupScreen(
+    strings = PrivateGroupStrings,
+    viewModel = viewModel,
+    addGroupDialog = { visible ->
+        // TODO
+    },
+    conversationScreen = { UiPlaceholder() }
+)
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/PrivateGroupStrings.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/PrivateGroupStrings.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e3994aee389ba072bd99b48ed6c7e2cb978040a3
--- /dev/null
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroup/PrivateGroupStrings.kt
@@ -0,0 +1,44 @@
+/*
+ * 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
+
+import org.briarproject.briar.desktop.group.GroupStrings
+import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
+import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18nF
+import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18nP
+
+// todo: replace with strings for private groups
+object PrivateGroupStrings : GroupStrings(
+    listTitle = i18n("forum.search.title"),
+    listDescription = i18n("access.forums.list"),
+    addButtonDescription = i18n("forum.add.title"),
+    noGroupsYet = i18n("forum.empty_state.text"),
+    noGroupSelectedTitle = i18n("forum.none_selected.title"),
+    noGroupSelectedText = i18n("forum.none_selected.hint"),
+    messageCount = { count ->
+        if (count > 0) i18nP("group.card.posts", count)
+        else i18n("group.card.no_posts")
+    },
+    unreadCount = { count ->
+        i18nP("access.forums.unread_count", count)
+    },
+    lastMessage = { timestamp ->
+        i18nF("access.forums.last_post_timestamp", timestamp)
+    },
+)
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroups/PrivateGroupCard.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroups/PrivateGroupCard.kt
deleted file mode 100644
index ec3deef2ce57b3e3993bfc01662ea6a9d5c652d9..0000000000000000000000000000000000000000
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroups/PrivateGroupCard.kt
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * 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.privategroups
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.defaultMinSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.offset
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.widthIn
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.Card
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import org.briarproject.briar.desktop.contact.ProfileCircle
-import org.briarproject.briar.desktop.theme.outline
-import org.briarproject.briar.desktop.theme.selectedCard
-import org.briarproject.briar.desktop.theme.surfaceVariant
-import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE
-import org.briarproject.briar.desktop.ui.HorizontalDivider
-import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18nF
-import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18nP
-import org.briarproject.briar.desktop.utils.TimeUtils.getFormattedTimestamp
-
-@Composable
-fun PrivateGroupCard(
-    privateGroupItem: PrivateGroupItem,
-    onSel: () -> Unit,
-    selected: Boolean,
-) {
-    val bgColor = if (selected) MaterialTheme.colors.selectedCard else MaterialTheme.colors.surfaceVariant
-    val outlineColor = MaterialTheme.colors.outline
-    val briarSecondary = MaterialTheme.colors.secondary
-
-    Card(
-        modifier = Modifier.fillMaxWidth().defaultMinSize(minHeight = HEADER_SIZE).clickable(onClick = onSel),
-        shape = RoundedCornerShape(0.dp),
-        backgroundColor = bgColor,
-        contentColor = MaterialTheme.colors.onSurface
-    ) {
-        Row(horizontalArrangement = Arrangement.SpaceBetween) {
-            Row(modifier = Modifier.align(Alignment.CenterVertically).padding(horizontal = 16.dp)) {
-                Box(modifier = Modifier.align(Alignment.CenterVertically)) {
-                    // TODO Do like `TextAvatarView` in Android
-                    ProfileCircle(36.dp, privateGroupItem.privateGroup.id.bytes)
-                    // Draw new message counter
-                    if (privateGroupItem.unread > 0) {
-                        Box(
-                            modifier = Modifier
-                                .align(Alignment.TopEnd)
-                                .offset(6.dp, (-6).dp)
-                                .height(20.dp)
-                                .widthIn(min = 20.dp, max = Dp.Infinity)
-                                .border(2.dp, outlineColor, CircleShape)
-                                .background(briarSecondary, CircleShape)
-                                .padding(horizontal = 6.dp)
-                        ) {
-                            Text(
-                                modifier = Modifier.align(Alignment.Center),
-                                fontSize = 8.sp,
-                                textAlign = TextAlign.Center,
-                                text = privateGroupItem.unread.toString(),
-                                maxLines = 1
-                            )
-                        }
-                    }
-                }
-                Column(modifier = Modifier.align(Alignment.CenterVertically).padding(start = 16.dp)) {
-                    Text(
-                        privateGroupItem.privateGroup.name,
-                        fontSize = 14.sp,
-                        modifier = Modifier.align(Alignment.Start).padding(bottom = 4.dp)
-                    )
-                    Text(
-                        i18nF("groups.card.created", privateGroupItem.privateGroup.creator.name),
-                        fontSize = 10.sp,
-                        modifier = Modifier.align(Alignment.Start).padding(bottom = 2.dp)
-                    )
-                    Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
-                        Text(
-                            i18nP("groups.card.messages", privateGroupItem.msgCount),
-                            fontSize = 10.sp,
-                        )
-                        Text(
-                            getFormattedTimestamp(privateGroupItem.timestamp),
-                            fontSize = 10.sp,
-                        )
-                    }
-                }
-            }
-        }
-    }
-    HorizontalDivider()
-}
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroups/PrivateGroupList.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroups/PrivateGroupList.kt
deleted file mode 100644
index 379ef6fea9c89037579b159bc28d21193557aa9f..0000000000000000000000000000000000000000
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroups/PrivateGroupList.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * 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.privategroups
-
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Scaffold
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import org.briarproject.bramble.api.sync.GroupId
-import org.briarproject.briar.desktop.theme.surfaceVariant
-import org.briarproject.briar.desktop.ui.Constants.COLUMN_WIDTH
-
-@Composable
-fun PrivateGroupList(
-    privateGroupList: List<PrivateGroupItem>,
-    isSelected: (GroupId) -> Boolean,
-    selectPrivateGroup: (GroupId) -> Unit,
-) {
-    // TODO AddPrivateGroupDialog
-    Scaffold(
-        modifier = Modifier.fillMaxHeight().width(COLUMN_WIDTH),
-        backgroundColor = MaterialTheme.colors.surfaceVariant,
-        // TODO SearchTextField
-        content = {
-            LazyColumn {
-                items(privateGroupList) { privateGroupItem ->
-                    PrivateGroupCard(
-                        privateGroupItem,
-                        { selectPrivateGroup(privateGroupItem.privateGroup.id) },
-                        isSelected(privateGroupItem.privateGroup.id)
-                    )
-                }
-            }
-        },
-    )
-}
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroups/PrivateGroupListViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroups/PrivateGroupListViewModel.kt
deleted file mode 100644
index 5863874f197d98c9f2b8344ddfa9aedf50d0cf52..0000000000000000000000000000000000000000
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroups/PrivateGroupListViewModel.kt
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * 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.privategroups
-
-import androidx.compose.runtime.State
-import androidx.compose.runtime.mutableStateListOf
-import androidx.compose.runtime.mutableStateOf
-import org.briarproject.bramble.api.connection.ConnectionRegistry
-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.sync.GroupId
-import org.briarproject.briar.api.conversation.ConversationManager
-import org.briarproject.briar.api.privategroup.PrivateGroupManager
-import org.briarproject.briar.desktop.threading.BriarExecutors
-import org.briarproject.briar.desktop.utils.clearAndAddAll
-import org.briarproject.briar.desktop.viewmodel.EventListenerDbViewModel
-import javax.inject.Inject
-
-class PrivateGroupListViewModel
-@Inject
-constructor(
-    private val privateGroupManager: PrivateGroupManager,
-    val conversationManager: ConversationManager,
-    val connectionRegistry: ConnectionRegistry,
-    briarExecutors: BriarExecutors,
-    lifecycleManager: LifecycleManager,
-    db: TransactionManager,
-    eventBus: EventBus,
-) : EventListenerDbViewModel(briarExecutors, lifecycleManager, db, eventBus) {
-
-    private val _fullPrivateGroupList = mutableStateListOf<PrivateGroupItem>()
-
-    val privateGroupList: List<PrivateGroupItem> = _fullPrivateGroupList
-
-    private fun loadPrivateGroups() {
-        val privateGroupList = mutableListOf<PrivateGroupItem>()
-        runOnDbThreadWithTransaction(true) { txn ->
-            privateGroupList.addAll(
-                privateGroupManager.getPrivateGroups(txn).map { privateGroup ->
-                    PrivateGroupItem(
-                        privateGroup,
-                        privateGroupManager.getGroupCount(txn, privateGroup.id),
-                    )
-                }
-            )
-            txn.attach {
-                _fullPrivateGroupList.clearAndAddAll(privateGroupList)
-            }
-        }
-    }
-
-    override fun onInit() {
-        super.onInit()
-        loadPrivateGroups()
-    }
-
-    private val _selectedContactId = mutableStateOf<GroupId?>(null)
-
-    val selectedPrivateGroupId: State<GroupId?> = _selectedContactId
-
-    fun selectPrivateGroup(privateGroupId: GroupId) {
-        _selectedContactId.value = privateGroupId
-    }
-
-    fun isSelected(privateGroupId: GroupId) = _selectedContactId.value == privateGroupId
-
-    override fun eventOccurred(e: Event) {
-        // TODO
-    }
-}
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroups/ThreadedConversationViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroups/ThreadedConversationViewModel.kt
deleted file mode 100644
index 32c94189e0e65dd70b1d72ed83bf9b2d7f2d437a..0000000000000000000000000000000000000000
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/privategroups/ThreadedConversationViewModel.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * 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.privategroups
-
-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.briar.api.conversation.ConversationManager
-import org.briarproject.briar.api.messaging.MessagingManager
-import org.briarproject.briar.api.messaging.PrivateMessageFactory
-import org.briarproject.briar.api.privategroup.PrivateGroupManager
-import org.briarproject.briar.desktop.threading.BriarExecutors
-import org.briarproject.briar.desktop.viewmodel.EventListenerDbViewModel
-import java.util.logging.Logger
-import javax.inject.Inject
-
-class ThreadedConversationViewModel
-@Inject
-constructor(
-    private val privateGroupManager: PrivateGroupManager,
-    private val conversationManager: ConversationManager,
-    private val messagingManager: MessagingManager,
-    private val privateMessageFactory: PrivateMessageFactory,
-    briarExecutors: BriarExecutors,
-    lifecycleManager: LifecycleManager,
-    db: TransactionManager,
-    eventBus: EventBus,
-) : EventListenerDbViewModel(briarExecutors, lifecycleManager, db, eventBus) {
-
-    companion object {
-        private val LOG = Logger.getLogger(ThreadedConversationViewModel::class.java.name)
-    }
-
-    override fun eventOccurred(e: Event) {
-        // TODO
-    }
-}
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/MainScreen.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/MainScreen.kt
index 77a754c684209426c0b6af0863e3e8cc9d91f479..bd2ca81b882aa52bc4d6bc7d5a268a139b25f8e7 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/MainScreen.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/MainScreen.kt
@@ -32,7 +32,7 @@ import org.briarproject.briar.desktop.forums.ForumScreen
 import org.briarproject.briar.desktop.mailbox.MailboxScreen
 import org.briarproject.briar.desktop.navigation.BriarSidebar
 import org.briarproject.briar.desktop.navigation.SidebarViewModel
-import org.briarproject.briar.desktop.privategroups.PrivateGroupScreen
+import org.briarproject.briar.desktop.privategroup.PrivateGroupScreen
 import org.briarproject.briar.desktop.settings.SettingsScreen
 import org.briarproject.briar.desktop.viewmodel.viewModel
 
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 595cab6a4728c9a8b1e7dab4c9dd5e5af89a1b28..f3f1c99d0781652372b6b41b6ce7c217ecf7de3b 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
@@ -1,6 +1,6 @@
 /*
  * Briar Desktop
- * Copyright (C) 2021-2022 The Briar Project
+ * 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
@@ -25,14 +25,13 @@ import dagger.multibindings.IntoMap
 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.ForumListViewModel
 import org.briarproject.briar.desktop.forums.sharing.ForumSharingViewModel
 import org.briarproject.briar.desktop.introduction.IntroductionViewModel
 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.privategroups.PrivateGroupListViewModel
-import org.briarproject.briar.desktop.privategroups.ThreadedConversationViewModel
+import org.briarproject.briar.desktop.privategroup.PrivateGroupListViewModel
 import org.briarproject.briar.desktop.settings.SettingsViewModel
 import kotlin.reflect.KClass
 
@@ -78,13 +77,8 @@ abstract class ViewModelModule {
 
     @Binds
     @IntoMap
-    @ViewModelKey(ForumViewModel::class)
-    abstract fun bindForumsViewModel(forumViewModel: ForumViewModel): ViewModel
-
-    @Binds
-    @IntoMap
-    @ViewModelKey(ThreadedConversationViewModel::class)
-    abstract fun bindThreadedConversationViewModel(threadedConversationViewModel: ThreadedConversationViewModel): ViewModel
+    @ViewModelKey(ForumListViewModel::class)
+    abstract fun bindForumListViewModel(forumListViewModel: ForumListViewModel): ViewModel
 
     @Binds
     @IntoMap