diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt
index be304ce31c545f63455b93d24b567d73b9b7f57d..c2b33acd9b8ce8e2a57f545cc017611b01b7ffb4 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt
@@ -44,6 +44,7 @@ import androidx.compose.ui.unit.dp
 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.SearchTextField
 import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
 
 @Composable
@@ -70,6 +71,7 @@ fun ContactList(
                     placeholder = i18n("contacts.search.title"),
                     icon = Icons.Filled.PersonAdd,
                     searchValue = filterBy,
+                    addButtonDescription = i18n("access.contacts.add"),
                     onValueChange = setFilterBy,
                     onAddButtonClicked = onContactAdd,
                 )
@@ -89,7 +91,11 @@ fun ContactList(
                             contactItem,
                             onSel = { selectContact(contactItem) },
                             selected = isSelected(contactItem),
-                            onRemovePending = { if (contactItem is PendingContactItem) removePendingContact(contactItem) },
+                            onRemovePending = {
+                                if (contactItem is PendingContactItem) {
+                                    removePendingContact(contactItem)
+                                }
+                            },
                         )
                     }
                 }
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 26511da0f91cd29d2cd0fde54df6fb1dcec73ec3..c0f6e74bc5621b8fa2ebfb7c75eb158a5ad1b86a 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
@@ -52,10 +52,10 @@ import androidx.compose.ui.window.rememberDialogState
 import org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH
 import org.briarproject.briar.desktop.utils.AccessibilityUtils.description
 import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
-import org.briarproject.briar.desktop.utils.PreviewUtils
+import org.briarproject.briar.desktop.utils.PreviewUtils.preview
 import java.awt.Dimension
 
-fun main() = PreviewUtils.preview {
+fun main() = preview {
     val visible = mutableStateOf(true)
     AddForumDialog(visible, {}, { visible.value = false })
 }
@@ -66,13 +66,13 @@ fun AddForumDialog(
     onCreate: (String) -> Unit,
     onCancelButtonClicked: () -> Unit,
 ) {
+    if (!visible.value) return
     Dialog(
         title = i18n("forum.add.title"),
         onCloseRequest = onCancelButtonClicked,
         state = rememberDialogState(
             position = WindowPosition(Alignment.Center),
         ),
-        visible = visible.value,
     ) {
         window.minimumSize = Dimension(360, 180)
         val scaffoldState = rememberScaffoldState()
@@ -101,7 +101,6 @@ fun AddForumDialog(
                         okButtonEnabled = isValidForumName(name.value),
                         onOkButtonClicked = {
                             onCreate(name.value)
-                            name.value = ""
                         },
                         onCancelButtonClicked = onCancelButtonClicked,
                     )
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumsItem.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumItem.kt
similarity index 55%
rename from briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumsItem.kt
rename to briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumItem.kt
index a8765ab1cd793ff2f80e3122726f72fe6a448acc..b3b4820f949ae91d285faed21e3ef3141f2a169d 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumsItem.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumItem.kt
@@ -18,9 +18,16 @@
 
 package org.briarproject.briar.desktop.forums
 
+import androidx.compose.ui.text.AnnotatedString
 import org.briarproject.bramble.api.sync.GroupId
 import org.briarproject.briar.api.client.MessageTracker
 import org.briarproject.briar.api.forum.Forum
+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.getFormattedTimestamp
+import org.briarproject.briar.desktop.utils.appendCommaSeparated
+import org.briarproject.briar.desktop.utils.buildBlankAnnotatedString
 
 interface GroupItem {
     val id: GroupId
@@ -28,9 +35,10 @@ interface GroupItem {
     val msgCount: Int
     val unread: Int
     val timestamp: Long
+    val description: AnnotatedString
 }
 
-data class ForumsItem(
+data class ForumItem(
     val forum: Forum,
     override val msgCount: Int,
     override val unread: Int,
@@ -46,4 +54,25 @@ data class ForumsItem(
 
     override val id: GroupId get() = forum.id
     override val name: String get() = forum.name
+
+    override val description: AnnotatedString
+        get() = buildBlankAnnotatedString {
+            append(name)
+            if (unread > 0) appendCommaSeparated(i18nP("access.forums.unread_count", unread))
+            if (msgCount == 0) appendCommaSeparated(i18n("group.card.no_posts"))
+            else appendCommaSeparated(
+                i18nF(
+                    "access.forums.last_message_timestamp",
+                    getFormattedTimestamp(timestamp)
+                )
+            )
+        }
+
+    override fun equals(other: Any?): Boolean {
+        return other is ForumItem && other.id == id
+    }
+
+    override fun hashCode(): Int {
+        return forum.hashCode()
+    }
 }
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumsScreen.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumScreen.kt
similarity index 94%
rename from briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumsScreen.kt
rename to briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumScreen.kt
index 1b4cca977de2b246e187208e271bec30116d4ec7..55b598364c8b765cd9938d27a0adc3ed39abba62 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumsScreen.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumScreen.kt
@@ -36,8 +36,8 @@ import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
 import org.briarproject.briar.desktop.viewmodel.viewModel
 
 @Composable
-fun ForumsScreen(
-    viewModel: ForumsViewModel = viewModel(),
+fun ForumScreen(
+    viewModel: ForumViewModel = viewModel(),
 ) {
     val addDialogVisible = remember { mutableStateOf(false) }
     AddForumDialog(
@@ -49,12 +49,12 @@ fun ForumsScreen(
         onCancelButtonClicked = { addDialogVisible.value = false }
     )
 
-    if (viewModel.groupList.value.isEmpty()) {
+    if (viewModel.forumList.value.isEmpty()) {
         NoForumsYet { addDialogVisible.value = true }
     } else {
         Row(modifier = Modifier.fillMaxWidth()) {
-            ForumsList(
-                list = viewModel.groupList,
+            GroupListComposable(
+                list = viewModel.forumList,
                 isSelected = viewModel::isSelected,
                 filterBy = viewModel.filterBy,
                 onFilterSet = viewModel::setFilterBy,
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumsViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumViewModel.kt
similarity index 91%
rename from briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumsViewModel.kt
rename to briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumViewModel.kt
index 8755aa850d50cbcab3ee722f07324ad19ac0fda9..7b70e4b40c4a64615b31a1f09b7815d7ea352a82 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumsViewModel.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumViewModel.kt
@@ -18,7 +18,6 @@
 
 package org.briarproject.briar.desktop.forums
 
-import androidx.compose.runtime.State
 import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.mutableStateListOf
 import androidx.compose.runtime.mutableStateOf
@@ -57,7 +56,7 @@ class Loaded(
     val posts: MutableList<ThreadItem> get() = messageTree.depthFirstOrder()
 }
 
-class ForumsViewModel @Inject constructor(
+class ForumViewModel @Inject constructor(
     private val forumManager: ForumManager,
     private val identityManager: IdentityManager,
     private val clock: Clock,
@@ -68,22 +67,22 @@ class ForumsViewModel @Inject constructor(
     eventBus: EventBus,
 ) : EventListenerDbViewModel(briarExecutors, lifecycleManager, db, eventBus) {
 
-    private val _fullGroupList = mutableStateListOf<ForumsItem>()
-    val groupList = derivedStateOf {
+    private val _fullForumList = mutableStateListOf<ForumItem>()
+    val forumList = derivedStateOf {
         val filter = _filterBy.value
-        _fullGroupList.filter { item ->
+        _fullForumList.filter { item ->
             item.name.contains(filter, ignoreCase = true)
         }.sortedByDescending { it.timestamp }
     }
 
     private val _selectedGroupItem = mutableStateOf<GroupItem?>(null)
-    val selectedGroupItem: State<GroupItem?> = _selectedGroupItem
+    val selectedGroupItem = _selectedGroupItem.asState()
 
     private val _filterBy = mutableStateOf("")
     val filterBy = _filterBy.asState()
 
     private val _posts = mutableStateOf<PostsState>(Loading)
-    val posts: State<PostsState> = _posts
+    val posts = _posts.asState()
 
     override fun onInit() {
         super.onInit()
@@ -108,13 +107,13 @@ class ForumsViewModel @Inject constructor(
     private fun loadGroups() {
         runOnDbThreadWithTransaction(true) { txn ->
             val list = forumManager.getForums(txn).map { forums ->
-                ForumsItem(
-                    forums,
-                    forumManager.getGroupCount(txn, forums.id),
+                ForumItem(
+                    forum = forums,
+                    groupCount = forumManager.getGroupCount(txn, forums.id),
                 )
             }
             txn.attach {
-                _fullGroupList.clearAndAddAll(list)
+                _fullForumList.clearAndAddAll(list)
             }
         }
     }
@@ -174,6 +173,6 @@ class ForumsViewModel @Inject constructor(
     }
 
     fun deleteGroup(groupItem: GroupItem) {
-        forumManager.removeForum((groupItem as ForumsItem).forum)
+        forumManager.removeForum((groupItem as ForumItem).forum)
     }
 }
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupsCard.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupCard.kt
similarity index 86%
rename from briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupsCard.kt
rename to briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupCard.kt
index 68f3a255c00d2a4d22de957c31674054dd3c89fc..7ef6bb884bcbe7ccd2ac242956a2f7a0d24dc0b0 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupsCard.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupCard.kt
@@ -18,13 +18,13 @@
 
 package org.briarproject.briar.desktop.forums
 
-import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Arrangement.SpaceBetween
 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.padding
+import androidx.compose.foundation.selection.selectable
 import androidx.compose.foundation.selection.selectableGroup
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.Card
@@ -35,8 +35,11 @@ import androidx.compose.ui.Alignment
 import androidx.compose.ui.Alignment.Companion.CenterVertically
 import androidx.compose.ui.Alignment.Companion.Start
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.semantics.contentDescription
 import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.text
+import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.unit.dp
 import org.briarproject.bramble.api.sync.GroupId
 import org.briarproject.briar.desktop.theme.selectedCard
@@ -44,13 +47,14 @@ import org.briarproject.briar.desktop.theme.surfaceVariant
 import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE
 import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
 import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18nP
-import org.briarproject.briar.desktop.utils.PreviewUtils
+import org.briarproject.briar.desktop.utils.PreviewUtils.preview
 import org.briarproject.briar.desktop.utils.TimeUtils.getFormattedTimestamp
+import org.briarproject.briar.desktop.utils.buildBlankAnnotatedString
 
 @Suppress("HardCodedStringLiteral")
-fun main() = PreviewUtils.preview {
+fun main() = preview {
     Column(Modifier.selectableGroup()) {
-        GroupsCard(
+        GroupCard(
             item = object : GroupItem {
                 override val id: GroupId = GroupId(getRandomId())
                 override val name: String =
@@ -58,6 +62,7 @@ fun main() = PreviewUtils.preview {
                 override val msgCount: Int = 42
                 override val unread: Int = 23
                 override val timestamp: Long = System.currentTimeMillis()
+                override val description: AnnotatedString = buildBlankAnnotatedString { }
             },
             onGroupItemSelected = {},
             selected = false,
@@ -66,7 +71,7 @@ fun main() = PreviewUtils.preview {
 }
 
 @Composable
-fun GroupsCard(
+fun GroupCard(
     item: GroupItem,
     onGroupItemSelected: (GroupItem) -> Unit,
     selected: Boolean,
@@ -75,7 +80,7 @@ fun GroupsCard(
         modifier = Modifier
             .fillMaxWidth()
             .defaultMinSize(minHeight = HEADER_SIZE)
-            .clickable(onClick = { onGroupItemSelected(item) })
+            .selectable(selected, onClick = { onGroupItemSelected(item) }, role = Role.Button)
             .semantics {
                 contentDescription =
                     if (selected) i18n("access.list.selected.yes")
@@ -90,7 +95,11 @@ fun GroupsCard(
         contentColor = MaterialTheme.colors.onSurface,
     ) {
         Row(
-            modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
+            modifier = Modifier
+                .padding(horizontal = 8.dp, vertical = 4.dp)
+                .semantics {
+                    text = item.description
+                },
         ) {
             GroupCircle(item, modifier = Modifier.align(Alignment.Top).padding(vertical = 12.dp))
             Column(
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/forums/GroupCircle.kt
index 76874e0a7b603285e487dd3682bb1b30f3f2e1ea..c3b244d2598b87e7ad8f6a216c11f4448a404934 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupCircle.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupCircle.kt
@@ -39,6 +39,7 @@ import androidx.compose.ui.unit.sp
 import org.briarproject.bramble.api.sync.GroupId
 import org.briarproject.briar.desktop.theme.outline
 import org.briarproject.briar.desktop.ui.NumberBadge
+import org.briarproject.briar.desktop.utils.InternationalizationUtils.locale
 
 @Composable
 fun GroupCircle(item: GroupItem, showMessageCount: Boolean = true, modifier: Modifier = Modifier) {
@@ -54,7 +55,7 @@ fun GroupCircle(item: GroupItem, showMessageCount: Boolean = true, modifier: Mod
                 .background(item.id.getBackgroundColor()),
         ) {
             Text(
-                text = item.name.substring(0..0).uppercase(),
+                text = item.name.substring(0..0).uppercase(locale),
                 color = Color.White,
                 style = MaterialTheme.typography.body1.copy(
                     fontSize = 24.sp,
@@ -75,12 +76,12 @@ fun GroupCircle(item: GroupItem, showMessageCount: Boolean = true, modifier: Mod
 
 fun GroupId.getBackgroundColor(): Color {
     return Color(
-        red = getByte(bytes, 0) * 3 / 4 + 96,
-        green = getByte(bytes, 1) * 3 / 4 + 96,
-        blue = getByte(bytes, 2) * 3 / 4 + 96,
+        red = bytes.getByte(0) * 3 / 4 + 96,
+        green = bytes.getByte(1) * 3 / 4 + 96,
+        blue = bytes.getByte(2) * 3 / 4 + 96,
     )
 }
 
-private fun getByte(bytes: ByteArray, index: Int): Byte {
-    return bytes[index % bytes.size]
+private fun ByteArray.getByte(index: Int): Byte {
+    return this[index % size]
 }
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 679d8a76f72a738733f0fb3cd792c1f8bc18c5d2..600b8fbd17cb4d12172027e2edbb4e0ee33e1ce3 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
@@ -60,7 +60,7 @@ import org.briarproject.briar.desktop.viewmodel.viewModel
 @Composable
 fun GroupConversationScreen(
     groupItem: GroupItem,
-    viewModel: ForumsViewModel = viewModel(),
+    viewModel: ForumViewModel = viewModel(),
 ) {
     val selectedPost = remember { mutableStateOf<MessageId?>(null) }
     Scaffold(
@@ -87,7 +87,7 @@ private fun GroupConversationHeader(
     groupItem: GroupItem,
     onGroupDelete: () -> Unit,
 ) {
-    val deleteGroupDialogState = remember { mutableStateOf(false) }
+    val deleteGroupDialogVisible = remember { mutableStateOf(false) }
     val menuState = remember { mutableStateOf(CLOSED) }
     val close = { menuState.value = CLOSED }
     Box(modifier = Modifier.fillMaxWidth().height(HEADER_SIZE + 1.dp)) {
@@ -130,7 +130,7 @@ private fun GroupConversationHeader(
                     DropdownMenuItem(
                         onClick = {
                             close()
-                            deleteGroupDialogState.value = true
+                            deleteGroupDialogVisible.value = true
                         }
                     ) {
                         Text(
@@ -143,9 +143,9 @@ private fun GroupConversationHeader(
         }
         HorizontalDivider(modifier = Modifier.align(BottomCenter))
     }
-    if (deleteGroupDialogState.value) {
+    if (deleteGroupDialogVisible.value) {
         DeleteForumDialog(
-            close = { deleteGroupDialogState.value = false },
+            close = { deleteGroupDialogVisible.value = false },
             onDelete = onGroupDelete,
         )
     }
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 152c39e77b5d27cab8c007dcd8cc7c1becae2d9d..1b1b7120e949613b14de6922a9b1db78546baec6 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
@@ -73,7 +73,7 @@ fun GroupInputComposable(
                     text = if (selectedPost.value == null) {
                         i18n("forum.message.hint")
                     } else {
-                        i18n("forum.message.replay.hint")
+                        i18n("forum.message.reply.hint")
                     },
                     style = MaterialTheme.typography.body1,
                 )
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumsList.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupListComposable.kt
similarity index 88%
rename from briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumsList.kt
rename to briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupListComposable.kt
index e90dd40aea326ce21b11bf710d866a714c7a167c..7ced0abe86fdff5e0996e1ba559ed226b21d7f73 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ForumsList.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/GroupListComposable.kt
@@ -39,17 +39,19 @@ 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
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.unit.dp
 import org.briarproject.bramble.api.sync.GroupId
-import org.briarproject.briar.desktop.contact.SearchTextField
 import org.briarproject.briar.desktop.theme.surfaceVariant
 import org.briarproject.briar.desktop.ui.Constants
 import org.briarproject.briar.desktop.ui.Constants.COLUMN_WIDTH
 import org.briarproject.briar.desktop.ui.HorizontalDivider
+import org.briarproject.briar.desktop.ui.SearchTextField
 import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
 
 @Composable
-fun ForumsList(
+fun GroupListComposable(
     list: State<List<GroupItem>>,
     isSelected: (GroupId) -> Boolean,
     filterBy: State<String>,
@@ -70,6 +72,7 @@ fun ForumsList(
                     placeholder = i18n("forum.search.title"),
                     icon = Icons.Filled.AddComment,
                     searchValue = filterBy.value,
+                    addButtonDescription = i18n("forum.add.title"),
                     onValueChange = onFilterSet,
                     onAddButtonClicked = onAddButtonClicked,
                 )
@@ -77,10 +80,13 @@ fun ForumsList(
             Box(modifier = Modifier.fillMaxSize()) {
                 LazyColumn(
                     state = scrollState,
-                    modifier = Modifier.selectableGroup()
+                    modifier = Modifier
+                        .semantics {
+                            contentDescription = i18n("access.forums.list")
+                        }.selectableGroup()
                 ) {
                     items(list.value) { item ->
-                        GroupsCard(
+                        GroupCard(
                             item = item,
                             onGroupItemSelected = onGroupItemSelected,
                             selected = isSelected(item.id)
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ThreadItem.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ThreadItem.kt
index bd1dc746ed71c64ae6408b379129eec083d693a3..ee751e90dd109bf6c4b54e350e227bce82a7a046 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ThreadItem.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/forums/ThreadItem.kt
@@ -55,21 +55,10 @@ abstract class ThreadItem(
     private var level: Int = UNDEFINED
     var isHighlighted = false
 
-    fun getLevel(): Int {
-        return level
-    }
-
-    override fun getId(): MessageId {
-        return messageId
-    }
-
-    override fun getParentId(): MessageId? {
-        return parentId
-    }
-
-    override fun getTimestamp(): Long {
-        return timestamp
-    }
+    override fun getId(): MessageId = messageId
+    override fun getParentId(): MessageId? = parentId
+    override fun getTimestamp(): Long = timestamp
+    fun getLevel(): Int = level
 
     /**
      * Returns the author's name, with an alias if one exists.
@@ -81,9 +70,7 @@ abstract class ThreadItem(
         this.level = level
     }
 
-    override fun hashCode(): Int {
-        return messageId.hashCode()
-    }
+    override fun hashCode(): Int = messageId.hashCode()
 
     override fun equals(other: Any?): Boolean {
         return other is ThreadItem && messageId == other.messageId
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threading/BriarExecutors.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threading/BriarExecutors.kt
index 545c12cd7969033e0df5fdb5f7ad82613e1501d2..e18685288a955c709acdd84e74bd37d5ec2c67b2 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threading/BriarExecutors.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threading/BriarExecutors.kt
@@ -30,6 +30,11 @@ interface BriarExecutors {
         @DatabaseExecutor task: (Transaction) -> Unit,
     )
 
+    suspend fun <T> runOnDbThread(
+        readOnly: Boolean,
+        @DatabaseExecutor task: (Transaction) -> T,
+    ): T
+
     fun onUiThread(@UiExecutor task: () -> Unit)
 
     fun onIoThread(@IoExecutor task: () -> Unit)
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threading/BriarExecutorsImpl.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threading/BriarExecutorsImpl.kt
index f002b48e0a265e5b889dc3e9f606db58d5ef8238..0700aa2092aefa6bd9f67e23ca8818b28ee197a6 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threading/BriarExecutorsImpl.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/threading/BriarExecutorsImpl.kt
@@ -20,6 +20,7 @@ package org.briarproject.briar.desktop.threading
 
 import mu.KotlinLogging
 import org.briarproject.bramble.api.db.DatabaseExecutor
+import org.briarproject.bramble.api.db.DbCallable
 import org.briarproject.bramble.api.db.Transaction
 import org.briarproject.bramble.api.db.TransactionManager
 import org.briarproject.bramble.api.lifecycle.IoExecutor
@@ -27,6 +28,8 @@ import org.briarproject.bramble.api.lifecycle.LifecycleManager
 import org.briarproject.briar.desktop.utils.KLoggerUtils.w
 import java.util.concurrent.Executor
 import javax.inject.Inject
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
 
 class BriarExecutorsImpl
 @Inject
@@ -75,6 +78,18 @@ constructor(
         }
     }
 
+    override suspend fun <T> runOnDbThread(
+        readOnly: Boolean,
+        @DatabaseExecutor task: (Transaction) -> T
+    ) = suspendCoroutine<T> { cont ->
+        // The coroutine suspends until the DatabaseExecutor has finished the task
+        // and ended the transaction. It then resumes with the returned value.
+        onDbThread {
+            val t = db.transactionWithResult(readOnly, DbCallable { txn -> task(txn) })
+            cont.resume(t)
+        }
+    }
+
     override fun onUiThread(@UiExecutor task: () -> Unit) = uiExecutor.execute(task)
 
     override fun onIoThread(@IoExecutor task: () -> Unit) = ioExecutor.execute(task)
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 43245ff4560f94e538aa4466531639a0f8af1a94..0e2dfd36d70490888ba8ce97b7497e99f6c2d8b8 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
@@ -21,7 +21,7 @@ package org.briarproject.briar.desktop.ui
 import androidx.compose.foundation.layout.Row
 import androidx.compose.runtime.Composable
 import org.briarproject.briar.desktop.conversation.PrivateMessageScreen
-import org.briarproject.briar.desktop.forums.ForumsScreen
+import org.briarproject.briar.desktop.forums.ForumScreen
 import org.briarproject.briar.desktop.navigation.BriarSidebar
 import org.briarproject.briar.desktop.navigation.SidebarViewModel
 import org.briarproject.briar.desktop.privategroups.PrivateGroupScreen
@@ -45,7 +45,7 @@ fun MainScreen(viewModel: SidebarViewModel = viewModel()) {
         when (viewModel.uiMode.value) {
             UiMode.CONTACTS -> PrivateMessageScreen()
             UiMode.GROUPS -> PrivateGroupScreen()
-            UiMode.FORUMS -> ForumsScreen()
+            UiMode.FORUMS -> ForumScreen()
             UiMode.SETTINGS -> SettingsScreen()
             UiMode.ABOUT -> AboutScreen()
             else -> UiPlaceholder()
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/SearchTextField.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/SearchTextField.kt
similarity index 93%
rename from briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/SearchTextField.kt
rename to briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/SearchTextField.kt
index 6d3e564dfb76184dfd532e37f97ecea736287ae5..b61a0466abd269944416fe7c0b46b42f18f1ba1d 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/SearchTextField.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/SearchTextField.kt
@@ -16,7 +16,7 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-package org.briarproject.briar.desktop.contact
+package org.briarproject.briar.desktop.ui
 
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.padding
@@ -32,7 +32,6 @@ import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.unit.dp
-import org.briarproject.briar.desktop.ui.ColoredIconButton
 import org.briarproject.briar.desktop.utils.AccessibilityUtils.description
 import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
 
@@ -41,6 +40,7 @@ fun SearchTextField(
     placeholder: String,
     icon: ImageVector,
     searchValue: String,
+    addButtonDescription: String,
     onValueChange: (String) -> Unit,
     onAddButtonClicked: () -> Unit,
 ) {
@@ -61,7 +61,7 @@ fun SearchTextField(
             ColoredIconButton(
                 icon = icon,
                 iconSize = 20.dp,
-                contentDescription = i18n("access.contacts.add"),
+                contentDescription = addButtonDescription,
                 onClick = onAddButtonClicked,
                 modifier = Modifier.padding(end = 8.dp)
             )
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/DbViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/DbViewModel.kt
index b82cb09a2fa6579730447c577bcf455dda25fd20..8547165856fb888d493aec9fd1cd1524b39d764c 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/DbViewModel.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/viewmodel/DbViewModel.kt
@@ -18,26 +18,18 @@
 
 package org.briarproject.briar.desktop.viewmodel
 
-import mu.KotlinLogging
 import org.briarproject.bramble.api.db.DatabaseExecutor
-import org.briarproject.bramble.api.db.DbCallable
 import org.briarproject.bramble.api.db.Transaction
 import org.briarproject.bramble.api.db.TransactionManager
 import org.briarproject.bramble.api.lifecycle.LifecycleManager
 import org.briarproject.briar.desktop.threading.BriarExecutors
-import kotlin.coroutines.resume
-import kotlin.coroutines.suspendCoroutine
 
 abstract class DbViewModel(
     private val briarExecutors: BriarExecutors,
     private val lifecycleManager: LifecycleManager,
-    private val db: TransactionManager
+    private val db: TransactionManager,
 ) : ViewModel {
 
-    companion object {
-        private val LOG = KotlinLogging.logger {}
-    }
-
     /**
      * Waits for the DB to open and runs the given [task] on the [DatabaseExecutor].
      * To avoid inconsistent state between the database and the UI
@@ -54,16 +46,15 @@ abstract class DbViewModel(
      */
     protected fun runOnDbThreadWithTransaction(
         readOnly: Boolean,
-        task: (Transaction) -> Unit
+        task: (Transaction) -> Unit,
     ) = briarExecutors.onDbThreadWithTransaction(readOnly, task)
 
+    /**
+     * Waits for the DB to open and runs the given [task] on the [DatabaseExecutor],
+     * returning its result.
+     */
     protected suspend fun <T> runOnDbThread(
         readOnly: Boolean,
-        task: (Transaction) -> T
-    ) = suspendCoroutine<T> { cont ->
-        briarExecutors.onDbThread {
-            val t = db.transactionWithResult(readOnly, DbCallable { txn -> task(txn) })
-            cont.resume(t)
-        }
-    }
+        @DatabaseExecutor task: (Transaction) -> T,
+    ): T = briarExecutors.runOnDbThread(readOnly, task)
 }
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 9f29948d41552c0491696786e78b53717e7904f9..bcb43539967d35b874c42033ecf5b643927ed529 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
@@ -25,7 +25,7 @@ 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.ForumsViewModel
+import org.briarproject.briar.desktop.forums.ForumViewModel
 import org.briarproject.briar.desktop.introduction.IntroductionViewModel
 import org.briarproject.briar.desktop.login.StartupViewModel
 import org.briarproject.briar.desktop.navigation.SidebarViewModel
@@ -76,8 +76,8 @@ abstract class ViewModelModule {
 
     @Binds
     @IntoMap
-    @ViewModelKey(ForumsViewModel::class)
-    abstract fun bindForumsViewModel(forumsViewModel: ForumsViewModel): ViewModel
+    @ViewModelKey(ForumViewModel::class)
+    abstract fun bindForumsViewModel(forumViewModel: ForumViewModel): ViewModel
 
     @Binds
     @IntoMap
diff --git a/briar-desktop/src/main/resources/strings/BriarDesktop.properties b/briar-desktop/src/main/resources/strings/BriarDesktop.properties
index 28a7b003cd1691f9bb38fa39dff7a96fd20b5427..0ccafe4e803b7f07a32ba9acceb40809e9f65661 100644
--- a/briar-desktop/src/main/resources/strings/BriarDesktop.properties
+++ b/briar-desktop/src/main/resources/strings/BriarDesktop.properties
@@ -60,6 +60,9 @@ access.settings.click_to_toggle_notifications=Click to toggle notifications
 access.return_to_previous_screen=Return to previous screen
 access.menu=Show menu
 access.forums.add=Add forum
+access.forums.list=forum list
+access.forums.unread_count={0, plural, one {one unread posts} other {{0} unread posts}}
+access.forums.last_message_timestamp=last message: {0}
 
 # Contacts
 contacts.none_selected.title=No contact selected
@@ -115,7 +118,7 @@ forum.delete.dialog.title=Confirm Leaving Forum
 forum.delete.dialog.message=Are you sure that you want to leave this forum?\n\nAny contacts you\'ve shared this forum with might stop receiving updates.
 forum.delete.dialog.button=Leave
 forum.message.hint=New Post
-forum.message.replay.hint=New Reply
+forum.message.reply.hint=New Reply
 group.card.no_posts=No posts
 group.card.posts={0, plural, one {{0} post} other {{0} posts}}