diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt
index bb0b796eb3f5c54b64770ee5db7b5af86e2bb9b4..738ffc8041d3c38cc2b321743271f0fb95a726c8 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/DesktopModule.kt
@@ -68,6 +68,8 @@ import org.briarproject.briar.desktop.threading.BriarExecutorsImpl
 import org.briarproject.briar.desktop.threading.UiExecutor
 import org.briarproject.briar.desktop.ui.BriarUi
 import org.briarproject.briar.desktop.ui.BriarUiImpl
+import org.briarproject.briar.desktop.ui.MessageCounter
+import org.briarproject.briar.desktop.ui.MessageCounterImpl
 import org.briarproject.briar.desktop.viewmodel.ViewModelModule
 import org.briarproject.briar.identity.IdentityModule
 import java.io.File
@@ -205,4 +207,8 @@ internal class DesktopModule(
     @Singleton
     internal fun provideNotificationProvider(): NotificationProvider =
         if (isLinux()) LibnotifyNotificationProvider else StubNotificationProvider
+
+    @Provides
+    @Singleton
+    internal fun provideMessageCounter(messageCounter: MessageCounterImpl): MessageCounter = messageCounter
 }
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt
index d0f935fc286aba751006bf9b5d1f11b125c89eb6..2a8e0a1eede1978d673530da6ee6f0f38f373019 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt
@@ -52,7 +52,7 @@ import org.briarproject.bramble.api.identity.AuthorId
 import org.briarproject.briar.desktop.theme.selectedCard
 import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE
 import org.briarproject.briar.desktop.ui.HorizontalDivider
-import org.briarproject.briar.desktop.ui.MessageCounter
+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
@@ -168,8 +168,8 @@ private fun RealContactRow(contactItem: ContactItem) {
         ) {
             Box {
                 ProfileCircle(36.dp, contactItem)
-                MessageCounter(
-                    unread = contactItem.unread,
+                NumberBadge(
+                    num = contactItem.unread,
                     modifier = Modifier.align(Alignment.TopEnd).offset(6.dp, (-6).dp)
                 )
             }
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationList.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationList.kt
index bfc7c0406edddf628e9fadaf2a2caa790d05d341..5a39243773ea8d6719fb3ed2850c4020eda53892 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationList.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationList.kt
@@ -65,7 +65,7 @@ import org.briarproject.briar.desktop.theme.ChevronUp
 import org.briarproject.briar.desktop.theme.divider
 import org.briarproject.briar.desktop.ui.HorizontalDivider
 import org.briarproject.briar.desktop.ui.Loader
-import org.briarproject.briar.desktop.ui.MessageCounter
+import org.briarproject.briar.desktop.ui.NumberBadge
 import org.briarproject.briar.desktop.ui.isWindowFocused
 import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
 import org.briarproject.briar.desktop.utils.PreviewUtils.preview
@@ -295,7 +295,7 @@ fun UnreadMessagesFAB(
             Icons.Filled.ChevronUp else Icons.Filled.ChevronDown
         Icon(arrow, i18n("access.message.jump_to_unread"))
     }
-    MessageCounter(
+    NumberBadge(
         counter,
         Modifier.align(Alignment.TopEnd).offset(3.dp, (-3).dp)
     )
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/notification/NotificationProvider.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/notification/NotificationProvider.kt
index 8de9dce6c61f6c3aeb6f9e330fd7ba2cd77af705..4d4a3e78468dda4e1d8870f6ef8a1b080adeaf7f 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/notification/NotificationProvider.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/notification/NotificationProvider.kt
@@ -35,7 +35,7 @@ interface NotificationProvider {
 
     fun init()
     fun uninit()
-    fun notifyPrivateMessages(num: Int)
+    fun notifyPrivateMessages(num: Int, contacts: Int)
 }
 
 object StubNotificationProvider : NotificationProvider {
@@ -47,5 +47,5 @@ object StubNotificationProvider : NotificationProvider {
 
     override fun init() {}
     override fun uninit() {}
-    override fun notifyPrivateMessages(num: Int) {}
+    override fun notifyPrivateMessages(num: Int, contacts: Int) {}
 }
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/notification/linux/LibnotifyNotificationProvider.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/notification/linux/LibnotifyNotificationProvider.kt
index 65623e4bfebcd0d708158c41d4d9a250610d1be4..06728605d735f2aaa7b896fde2e2b92bb71d1677 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/notification/linux/LibnotifyNotificationProvider.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/notification/linux/LibnotifyNotificationProvider.kt
@@ -25,6 +25,7 @@ import org.briarproject.briar.desktop.notification.NotificationProvider
 import org.briarproject.briar.desktop.utils.AudioUtils.loadAudioFromResource
 import org.briarproject.briar.desktop.utils.AudioUtils.play
 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.KLoggerUtils.e
 import org.briarproject.briar.desktop.utils.KLoggerUtils.i
@@ -99,7 +100,7 @@ object LibnotifyNotificationProvider : NotificationProvider {
         }
     }
 
-    override fun notifyPrivateMessages(num: Int) {
+    override fun notifyPrivateMessages(num: Int, contacts: Int) {
         if (!libNotifyAvailable) {
             // play sound even if libnotify unavailable
             if (soundAvailable) sound.play()
@@ -116,7 +117,10 @@ object LibnotifyNotificationProvider : NotificationProvider {
          * The summary must be encoded using UTF-8.
          */
         // todo: we could use body instead with markup (where supported)
-        val text = i18nP("notifications.message.private", num)
+        val text = if (contacts == 1)
+            i18nP("notifications.message.private.one_chat", num)
+        else
+            i18nF("notifications.message.private.several_chats", num, contacts)
         val notification = libNotify.notify_notification_new(text, null, null)
 
         /**
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 5c0cae2b54cd8966eeb6c229fbbb6da812d1edd6..545c12cd7969033e0df5fdb5f7ad82613e1501d2 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
@@ -19,11 +19,17 @@
 package org.briarproject.briar.desktop.threading
 
 import org.briarproject.bramble.api.db.DatabaseExecutor
+import org.briarproject.bramble.api.db.Transaction
 import org.briarproject.bramble.api.lifecycle.IoExecutor
 
 interface BriarExecutors {
     fun onDbThread(@DatabaseExecutor task: () -> Unit)
 
+    fun onDbThreadWithTransaction(
+        readOnly: Boolean,
+        @DatabaseExecutor task: (Transaction) -> Unit,
+    )
+
     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 531f4273506fe57710e90c2fca63827bc8fceedd..f002b48e0a265e5b889dc3e9f606db58d5ef8238 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
@@ -18,8 +18,13 @@
 
 package org.briarproject.briar.desktop.threading
 
+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.lifecycle.IoExecutor
+import org.briarproject.bramble.api.lifecycle.LifecycleManager
+import org.briarproject.briar.desktop.utils.KLoggerUtils.w
 import java.util.concurrent.Executor
 import javax.inject.Inject
 
@@ -29,8 +34,46 @@ constructor(
     @UiExecutor private val uiExecutor: Executor,
     @DatabaseExecutor private val dbExecutor: Executor,
     @IoExecutor private val ioExecutor: Executor,
+    private val lifecycleManager: LifecycleManager,
+    private val db: TransactionManager,
 ) : BriarExecutors {
-    override fun onDbThread(@DatabaseExecutor task: () -> Unit) = dbExecutor.execute(task)
+
+    companion object {
+        private val LOG = KotlinLogging.logger {}
+    }
+
+    override fun onDbThread(@DatabaseExecutor task: () -> Unit) = dbExecutor.execute {
+        try {
+            lifecycleManager.waitForDatabase()
+            task()
+        } catch (e: InterruptedException) {
+            LOG.w { "Interrupted while waiting for database" }
+            Thread.currentThread().interrupt()
+        } catch (e: Exception) {
+            LOG.w(e) { "Unhandled exception in database executor" }
+        }
+    }
+
+    override fun onDbThreadWithTransaction(
+        readOnly: Boolean,
+        @DatabaseExecutor task: (Transaction) -> Unit,
+    ) = dbExecutor.execute {
+        try {
+            lifecycleManager.waitForDatabase()
+            val txn = db.startTransaction(readOnly)
+            try {
+                task(txn)
+                db.commitTransaction(txn)
+            } finally {
+                db.endTransaction(txn)
+            }
+        } catch (e: InterruptedException) {
+            LOG.w { "Interrupted while waiting for database" }
+            Thread.currentThread().interrupt()
+        } catch (e: Exception) {
+            LOG.w(e) { "Unhandled exception in database executor" }
+        }
+    }
 
     override fun onUiThread(@UiExecutor task: () -> Unit) = uiExecutor.execute(task)
 
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt
index 7b8ca936ec15d1efa67d668f99c0aa39a4a7776d..b7acda7f56929cd9c3fcbe71dcdc799ffbb51183 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt
@@ -45,8 +45,6 @@ import org.briarproject.bramble.api.event.EventListener
 import org.briarproject.bramble.api.lifecycle.LifecycleManager
 import org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING
 import org.briarproject.bramble.api.lifecycle.event.LifecycleEvent
-import org.briarproject.briar.api.conversation.event.ConversationMessageReceivedEvent
-import org.briarproject.briar.desktop.conversation.ConversationMessagesReadEvent
 import org.briarproject.briar.desktop.expiration.ExpirationBanner
 import org.briarproject.briar.desktop.login.ErrorScreen
 import org.briarproject.briar.desktop.login.StartupScreen
@@ -95,6 +93,7 @@ constructor(
     private val viewModelProvider: ViewModelProvider,
     private val configuration: Configuration,
     private val notificationProvider: NotificationProvider,
+    private val messageCounter: MessageCounterImpl,
 ) : BriarUi {
 
     private var screenState by mutableStateOf(
@@ -134,29 +133,11 @@ constructor(
                 .toAwtImage(LocalDensity.current, LocalLayoutDirection.current, Size(32f, 32f))
 
             DisposableEffect(Unit) {
-                // todo: hard-coded messageCount doesn't account for unread messages on application start
-                // also see https://code.briarproject.org/briar/briar-desktop/-/issues/133
-                var messageCount = 0
 
                 val eventListener = EventListener { e ->
                     when (e) {
                         is LifecycleEvent ->
                             if (e.lifecycleState == RUNNING) screenState = MAIN
-
-                        is ConversationMessageReceivedEvent<*> -> {
-                            messageCount++
-                            if (!focusState.focused) {
-                                window.iconImage = iconBadge
-                                if (configuration.showNotifications) {
-                                    notificationProvider.notifyPrivateMessages(messageCount)
-                                }
-                            }
-                        }
-
-                        is ConversationMessagesReadEvent -> {
-                            messageCount -= e.count
-                            if (messageCount < 0) messageCount = 0
-                        }
                     }
                 }
                 val focusListener = object : WindowFocusListener {
@@ -169,12 +150,22 @@ constructor(
                         focusState.focused = false
                     }
                 }
+                val messageCounterListener: MessageCounterListener = { (total, contacts) ->
+                    if (total > 0 && !focusState.focused) {
+                        window.iconImage = iconBadge
+                        if (configuration.showNotifications) {
+                            notificationProvider.notifyPrivateMessages(total, contacts)
+                        }
+                    }
+                }
 
                 notificationProvider.init()
                 eventBus.addListener(eventListener)
                 window.addWindowFocusListener(focusListener)
+                messageCounter.addListener(messageCounterListener)
 
                 onDispose {
+                    messageCounter.removeListener(messageCounterListener)
                     eventBus.removeListener(eventListener)
                     window.removeWindowFocusListener(focusListener)
                     notificationProvider.uninit()
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/MessageCounter.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/MessageCounter.kt
index 07f2c8d71f9b64e85901401ff4f8b77bd39f40b0..f01ff09f0a93345774d534a97a9f2ea3b2555750 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/MessageCounter.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/MessageCounter.kt
@@ -18,43 +18,16 @@
 
 package org.briarproject.briar.desktop.ui
 
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.widthIn
-import androidx.compose.foundation.shape.CircleShape
-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 org.briarproject.briar.desktop.theme.outline
+interface MessageCounter {
 
-@Composable
-fun MessageCounter(unread: Int, modifier: Modifier = Modifier) {
-    val outlineColor = MaterialTheme.colors.outline
-    val briarSecondary = MaterialTheme.colors.secondary
-    if (unread > 0) {
-        Box(
-            modifier = modifier
-                .height(20.dp)
-                .widthIn(min = 20.dp, max = Dp.Infinity)
-                .border(1.dp, outlineColor, CircleShape)
-                .background(briarSecondary, CircleShape)
-                .padding(horizontal = 6.dp)
-        ) {
-            Text(
-                modifier = Modifier.align(Alignment.Center),
-                style = MaterialTheme.typography.overline,
-                textAlign = TextAlign.Center,
-                text = unread.toString(),
-                maxLines = 1
-            )
-        }
-    }
+    fun addListener(listener: MessageCounterListener): Boolean
+
+    fun removeListener(listener: MessageCounterListener): Boolean
 }
+
+data class MessageCounterData(
+    val total: Int,
+    val contacts: Int,
+)
+
+typealias MessageCounterListener = (MessageCounterData) -> Unit
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/MessageCounterImpl.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/MessageCounterImpl.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2234e6812451a7f2992562292e4649ba25e030cc
--- /dev/null
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/MessageCounterImpl.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.ui
+
+import org.briarproject.bramble.api.Multiset
+import org.briarproject.bramble.api.contact.ContactId
+import org.briarproject.bramble.api.contact.ContactManager
+import org.briarproject.bramble.api.event.EventBus
+import org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING
+import org.briarproject.bramble.api.lifecycle.event.LifecycleEvent
+import org.briarproject.briar.api.conversation.ConversationManager
+import org.briarproject.briar.api.conversation.event.ConversationMessageReceivedEvent
+import org.briarproject.briar.desktop.conversation.ConversationMessagesReadEvent
+import org.briarproject.briar.desktop.threading.BriarExecutors
+import javax.inject.Inject
+
+class MessageCounterImpl
+@Inject
+constructor(
+    private val contactManager: ContactManager,
+    private val conversationManager: ConversationManager,
+    private val briarExecutors: BriarExecutors,
+    eventBus: EventBus,
+) : MessageCounter {
+
+    private val messageCount = Multiset<ContactId>()
+
+    private val listeners = mutableListOf<MessageCounterListener>()
+
+    init {
+        eventBus.addListener { e ->
+            when (e) {
+                is LifecycleEvent ->
+                    if (e.lifecycleState == RUNNING) {
+                        briarExecutors.onDbThreadWithTransaction(true) { txn ->
+                            val contacts = contactManager.getContacts(txn)
+                            for (c in contacts) {
+                                val unreadMessages = conversationManager.getGroupCount(txn, c.id).unreadCount
+                                messageCount.addCount(c.id, unreadMessages)
+                            }
+                            txn.attach { informListeners() }
+                        }
+                    }
+
+                is ConversationMessageReceivedEvent<*> -> {
+                    messageCount.add(e.contactId)
+                    informListeners()
+                }
+
+                is ConversationMessagesReadEvent -> {
+                    messageCount.removeCount(e.contactId, e.count)
+                }
+            }
+        }
+    }
+
+    override fun addListener(listener: MessageCounterListener) = listeners.add(listener)
+
+    override fun removeListener(listener: MessageCounterListener) = listeners.remove(listener)
+
+    private fun informListeners() = listeners.forEach { l ->
+        l.invoke(MessageCounterData(messageCount.total, messageCount.unique))
+    }
+
+    private fun <T> Multiset<T>.removeCount(t: T, count: Int) =
+        repeat(count) { remove(t) }
+
+    private fun <T> Multiset<T>.addCount(t: T, count: Int) =
+        repeat(count) { add(t) }
+}
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/NumberBadge.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/NumberBadge.kt
new file mode 100644
index 0000000000000000000000000000000000000000..564b66488107754347b316e0fe5e4033e8c495aa
--- /dev/null
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/ui/NumberBadge.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.ui
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.shape.CircleShape
+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 org.briarproject.briar.desktop.theme.outline
+
+@Composable
+fun NumberBadge(num: Int, modifier: Modifier = Modifier) {
+    val outlineColor = MaterialTheme.colors.outline
+    val briarSecondary = MaterialTheme.colors.secondary
+    if (num > 0) {
+        Box(
+            modifier = modifier
+                .height(20.dp)
+                .widthIn(min = 20.dp, max = Dp.Infinity)
+                .border(1.dp, outlineColor, CircleShape)
+                .background(briarSecondary, CircleShape)
+                .padding(horizontal = 6.dp)
+        ) {
+            Text(
+                modifier = Modifier.align(Alignment.Center),
+                style = MaterialTheme.typography.overline,
+                textAlign = TextAlign.Center,
+                text = num.toString(),
+                maxLines = 1
+            )
+        }
+    }
+}
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 1a09ad893ce4fb31ecc4c57bc98c027a9a0a34d3..73d439337dd4bc004c13cc29a5eb88f1c8f2a3ee 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
@@ -24,7 +24,6 @@ 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 org.briarproject.briar.desktop.utils.KLoggerUtils.w
 
 abstract class DbViewModel(
     private val briarExecutors: BriarExecutors,
@@ -42,17 +41,7 @@ abstract class DbViewModel(
      * whenever the UI should react to a successful transaction,
      * strongly consider using [runOnDbThreadWithTransaction] instead.
      */
-    protected fun runOnDbThread(task: () -> Unit) = briarExecutors.onDbThread {
-        try {
-            lifecycleManager.waitForDatabase()
-            task()
-        } catch (e: InterruptedException) {
-            LOG.w { "Interrupted while waiting for database" }
-            Thread.currentThread().interrupt()
-        } catch (e: Exception) {
-            LOG.w(e) { "Unhandled exception in database executor" }
-        }
-    }
+    protected fun runOnDbThread(task: () -> Unit) = briarExecutors.onDbThread(task)
 
     /**
      * Waits for the DB to open and runs the given [task] on the [DatabaseExecutor],
@@ -63,21 +52,5 @@ abstract class DbViewModel(
     protected fun runOnDbThreadWithTransaction(
         readOnly: Boolean,
         task: (Transaction) -> Unit
-    ) = briarExecutors.onDbThread {
-        try {
-            lifecycleManager.waitForDatabase()
-            val txn = db.startTransaction(readOnly)
-            try {
-                task(txn)
-                db.commitTransaction(txn)
-            } finally {
-                db.endTransaction(txn)
-            }
-        } catch (e: InterruptedException) {
-            LOG.w { "Interrupted while waiting for database" }
-            Thread.currentThread().interrupt()
-        } catch (e: Exception) {
-            LOG.w(e) { "Unhandled exception in database executor" }
-        }
-    }
+    ) = briarExecutors.onDbThreadWithTransaction(readOnly, task)
 }
diff --git a/briar-desktop/src/main/resources/strings/BriarDesktop.properties b/briar-desktop/src/main/resources/strings/BriarDesktop.properties
index c68aeec54288e94e9eb68e21eeaee0a77ff5449a..b7f24e82a2be50673a6c6e04bd51dec265811f36 100644
--- a/briar-desktop/src/main/resources/strings/BriarDesktop.properties
+++ b/briar-desktop/src/main/resources/strings/BriarDesktop.properties
@@ -255,7 +255,8 @@ expiration.banner.part1.nozero={0, plural, one {This is a test version of Briar
 expiration.banner.part2=Please update to a newer version in time.
 
 # Notification
-notifications.message.private={0, plural, one {New private message.} other {{0} new private messages.}}
+notifications.message.private.one_chat={0, plural, one {New private message.} other {{0} new private messages.}}
+notifications.message.private.several_chats={0} new messages in {1} private chats.
 
 # Settings
 settings.title=Settings
diff --git a/briar-desktop/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt b/briar-desktop/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt
index a043f8baa051d1b17dc41c42bd8eb76fe13a0ee6..7ac394f9433338a27bc40418a40b9b0edfb4ecdc 100644
--- a/briar-desktop/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt
+++ b/briar-desktop/src/test/kotlin/org/briarproject/briar/desktop/DesktopTestModule.kt
@@ -72,6 +72,8 @@ import org.briarproject.briar.desktop.threading.BriarExecutorsImpl
 import org.briarproject.briar.desktop.threading.UiExecutor
 import org.briarproject.briar.desktop.ui.BriarUi
 import org.briarproject.briar.desktop.ui.BriarUiImpl
+import org.briarproject.briar.desktop.ui.MessageCounter
+import org.briarproject.briar.desktop.ui.MessageCounterImpl
 import org.briarproject.briar.desktop.viewmodel.ViewModelModule
 import org.briarproject.briar.identity.IdentityModule
 import org.briarproject.briar.test.TestModule
@@ -212,6 +214,10 @@ internal class DesktopTestModule(
     internal fun provideNotificationProvider(): NotificationProvider =
         if (isLinux()) LibnotifyNotificationProvider else StubNotificationProvider
 
+    @Provides
+    @Singleton
+    internal fun provideMessageCounter(messageCounter: MessageCounterImpl): MessageCounter = messageCounter
+
     @Provides
     @Singleton
     internal fun provideTestAvatarCreator(testAvatarCreator: TestAvatarCreatorImpl): TestAvatarCreator {
diff --git a/briar-desktop/src/test/kotlin/org/briarproject/briar/desktop/notification/linux/TestNativeNotifications.kt b/briar-desktop/src/test/kotlin/org/briarproject/briar/desktop/notification/linux/TestNativeNotifications.kt
index 83e833d1361f16e0294805a80b6ba4e6aeb02ee2..dffcbebfaa3f3bced3fbd7906259a5d60cfe7d26 100644
--- a/briar-desktop/src/test/kotlin/org/briarproject/briar/desktop/notification/linux/TestNativeNotifications.kt
+++ b/briar-desktop/src/test/kotlin/org/briarproject/briar/desktop/notification/linux/TestNativeNotifications.kt
@@ -26,10 +26,10 @@ fun main() {
     LibnotifyNotificationProvider.apply {
         init()
 
-        notifyPrivateMessages(9)
+        notifyPrivateMessages(4, 1)
         Thread.sleep(1000)
 
-        notifyPrivateMessages(10)
+        notifyPrivateMessages(10, 3)
         Thread.sleep(1000)
 
         uninit()