diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/navigation/BriarSidebar.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/navigation/BriarSidebar.kt
index 44232a46fb81f1d7991b32e33a244ff0f68a7a0e..51ca936d49d9ee8bc66f59f9c80f6cb614fafb1f 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/navigation/BriarSidebar.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/navigation/BriarSidebar.kt
@@ -18,11 +18,14 @@
 
 package org.briarproject.briar.desktop.navigation
 
+import androidx.compose.foundation.Canvas
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredSize
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.selection.selectableGroup
 import androidx.compose.material.IconButton
@@ -55,20 +58,26 @@ fun BriarSidebar(
     account: LocalAuthor?,
     uiMode: UiMode,
     setUiMode: (UiMode) -> Unit,
+    messageCount: SidebarViewModel.MessageCount,
 ) {
-    val displayButton = @Composable { selectedMode: UiMode, mode: UiMode, icon: ImageVector ->
-        BriarSidebarButton(
-            selectedMode == mode,
-            { setUiMode(mode) },
-            icon,
-            i18n(mode.contentDescriptionKey)
-        )
-    }
+    @Composable
+    fun BriarSidebarButton(
+        mode: UiMode,
+        icon: ImageVector,
+        messageCount: Int = 0,
+    ) = BriarSidebarButton(
+        uiMode == mode,
+        { setUiMode(mode) },
+        icon,
+        i18n(mode.contentDescriptionKey),
+        messageCount
+    )
 
     Surface(
         modifier = Modifier.width(SIDEBAR_WIDTH).fillMaxHeight().selectableGroup(),
         color = MaterialTheme.colors.sidebarSurface
     ) {
+        val configuration = getConfiguration()
         Column(verticalArrangement = Arrangement.Top) {
             // profile button
             Box(
@@ -76,52 +85,28 @@ fun BriarSidebar(
             ) {
                 account?.let { ProfileCircle(size = 45.dp, it.id.bytes) }
             }
-            val modes = buildList {
-                add(Pair(UiMode.CONTACTS, Icons.Filled.Contacts))
-                val configuration = getConfiguration()
-                if (configuration.shouldEnablePrivateGroups()) add(Pair(UiMode.GROUPS, Icons.Filled.Group))
-                if (configuration.shouldEnableForums()) add(Pair(UiMode.FORUMS, Icons.Filled.Forum))
-                if (configuration.shouldEnableBlogs()) add(Pair(UiMode.BLOGS, Icons.Filled.ChromeReaderMode))
-            }
-            modes.forEach { (mode, icon) ->
-                displayButton(uiMode, mode, icon)
-            }
+
+            BriarSidebarButton(UiMode.CONTACTS, Icons.Filled.Contacts, messageCount.privateMessages)
+            if (configuration.shouldEnablePrivateGroups()) BriarSidebarButton(UiMode.GROUPS, Icons.Filled.Group)
+            if (configuration.shouldEnableForums()) BriarSidebarButton(UiMode.FORUMS, Icons.Filled.Forum, messageCount.forumPosts)
+            if (configuration.shouldEnableBlogs()) BriarSidebarButton(UiMode.BLOGS, Icons.Filled.ChromeReaderMode)
         }
         Column(verticalArrangement = Arrangement.Bottom) {
-            val modes = buildList {
-                val configuration = getConfiguration()
-                if (configuration.shouldEnableTransportSettings()) add(
-                    Pair(UiMode.TRANSPORTS, Icons.Filled.WifiTethering)
-                )
-                add(Pair(UiMode.SETTINGS, Icons.Filled.Settings))
-                add(Pair(UiMode.ABOUT, Icons.Filled.Info))
-            }
-            modes.forEach { (mode, icon) ->
-                displayButton(uiMode, mode, icon)
-            }
+            if (configuration.shouldEnableTransportSettings()) BriarSidebarButton(UiMode.TRANSPORTS, Icons.Filled.WifiTethering)
+            BriarSidebarButton(UiMode.SETTINGS, Icons.Filled.Settings)
+            BriarSidebarButton(UiMode.ABOUT, Icons.Filled.Info)
         }
     }
 }
 
-@Composable
-fun BriarSidebarButton(
-    mode: UiMode,
-    currentMode: UiMode,
-    setUiMode: (UiMode) -> Unit,
-) = BriarSidebarButton(
-    currentMode == mode,
-    { setUiMode(mode) },
-    mode.icon,
-    i18n(mode.contentDescriptionKey)
-)
-
 @Composable
 fun BriarSidebarButton(
     selected: Boolean,
     onClick: () -> Unit,
     icon: ImageVector,
     contentDescription: String,
-) {
+    messageCount: Int,
+) = Box {
     val tint = if (selected) MaterialTheme.colors.primary else MaterialTheme.colors.onSurface
     IconButton(
         icon = icon,
@@ -131,4 +116,10 @@ fun BriarSidebarButton(
         onClick = onClick,
         modifier = Modifier.padding(vertical = 4.dp, horizontal = 12.dp),
     )
+    if (messageCount > 0) {
+        val color = MaterialTheme.colors.secondary
+        Canvas(modifier = Modifier.align(Alignment.TopEnd).requiredSize(12.dp).offset((-12).dp, 12.dp)) {
+            drawCircle(color)
+        }
+    }
 }
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/navigation/SidebarViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/navigation/SidebarViewModel.kt
index 223e98e9ab737db652fe8dd3a5f615267411fa33..539da08caa51d7fa3b762d116a70713046a27f2a 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/navigation/SidebarViewModel.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/navigation/SidebarViewModel.kt
@@ -18,29 +18,53 @@
 
 package org.briarproject.briar.desktop.navigation
 
-import androidx.compose.runtime.State
 import androidx.compose.runtime.mutableStateOf
 import org.briarproject.bramble.api.identity.IdentityManager
 import org.briarproject.bramble.api.identity.LocalAuthor
+import org.briarproject.briar.desktop.ui.MessageCounter
+import org.briarproject.briar.desktop.ui.MessageCounterData
+import org.briarproject.briar.desktop.ui.MessageCounterDataType.Forum
+import org.briarproject.briar.desktop.ui.MessageCounterDataType.PrivateMessage
 import org.briarproject.briar.desktop.ui.UiMode
 import org.briarproject.briar.desktop.viewmodel.ViewModel
+import org.briarproject.briar.desktop.viewmodel.asState
+import org.briarproject.briar.desktop.viewmodel.update
 import javax.inject.Inject
 
 class SidebarViewModel
 @Inject
 constructor(
     private val identityManager: IdentityManager,
+    private val messageCounter: MessageCounter,
 ) : ViewModel() {
-
     override fun onInit() {
+        super.onInit()
         loadAccountInfo()
+        messageCounter.addListener(this::onMessageCounterUpdated)
+    }
+
+    override fun onCleared() {
+        super.onCleared()
+        messageCounter.removeListener(this::onMessageCounterUpdated)
+    }
+
+    private fun onMessageCounterUpdated(data: MessageCounterData) {
+        val (type, count) = data
+        when (type) {
+            PrivateMessage -> _messageCount.update { copy(privateMessages = count) }
+            Forum -> _messageCount.update { copy(forumPosts = count) }
+        }
     }
 
     private var _uiMode = mutableStateOf(UiMode.CONTACTS)
     private var _account = mutableStateOf<LocalAuthor?>(null)
 
-    val uiMode: State<UiMode> = _uiMode
-    val account: State<LocalAuthor?> = _account
+    private var _messageCount = mutableStateOf(MessageCount())
+
+    val uiMode = _uiMode.asState()
+    val account = _account.asState()
+
+    val messageCount = _messageCount.asState()
 
     fun setUiMode(uiMode: UiMode) {
         _uiMode.value = uiMode
@@ -49,4 +73,9 @@ constructor(
     fun loadAccountInfo() {
         _account.value = identityManager.localAuthor
     }
+
+    data class MessageCount(
+        val privateMessages: Int = 0,
+        val forumPosts: Int = 0,
+    )
 }
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 8badde07f6e3c2d1d46eb23793b84e131c741d76..9060f6b89e33b50dedca9f3e57f9512a0e4e5113 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
@@ -161,8 +161,8 @@ constructor(
                         lastNotificationForum = 0
                     }
                 }
-                val messageCounterListener: MessageCounterListener = { (type, total, groups) ->
-                    if (total > 0 && !focusState.focused) {
+                val messageCounterListener: MessageCounterListener = { (type, total, groups, inc) ->
+                    if (inc && total > 0 && !focusState.focused) {
                         val callback: NotificationProvider.() -> Unit = when (type) {
                             PrivateMessage -> { { notifyPrivateMessages(total, groups) } }
                             Forum -> { { notifyForumPosts(total, groups) } }
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 4dcdf9c52c98192b165f3c78641c0b546f2fb592..d80e2e12750a7f77f666d772679813287f4b932f 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
@@ -55,6 +55,7 @@ fun MainScreen(viewModel: SidebarViewModel = viewModel()) {
                     viewModel.account.value,
                     viewModel.uiMode.value,
                     viewModel::setUiMode,
+                    viewModel.messageCount.value,
                 )
                 VerticalDivider()
                 when (viewModel.uiMode.value) {
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 b5a07dfac71c34726bd80fc5c86a768f73b70dab..31cc4f6360bfd306df5e270c490d72997c1b9ebe 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
@@ -27,10 +27,26 @@ interface MessageCounter {
 
 enum class MessageCounterDataType { PrivateMessage, Forum }
 
+/**
+ * Data holder for MessageCounter updates.
+ */
 data class MessageCounterData(
+    /**
+     * Type of unread messages.
+     */
     val type: MessageCounterDataType,
+    /**
+     * Sum of all unread messages of the given [type].
+     */
     val total: Int,
+    /**
+     * Amount of different private chats/groups/forums (depending on [type]) with unread messages.
+     */
     val groups: Int,
+    /**
+     * If `true`, [total] has increased since the last time the listeners were informed.
+     */
+    val increment: Boolean,
 )
 
 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
index 906a40ebfb6ec22c3cae739e681f4879a21f1fb3..85edf544570f728f39b20096c64e8b735706291f 100644
--- 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
@@ -68,28 +68,30 @@ constructor(
                                 countForumPosts.addCount(f.id, unreadMessages)
                             }
                             txn.attach {
-                                informListeners(PrivateMessage)
-                                informListeners(Forum)
+                                informListeners(PrivateMessage, true)
+                                informListeners(Forum, true)
                             }
                         }
                     }
 
                 is ConversationMessageReceivedEvent<*> -> {
                     countPrivateMessages.add(e.contactId)
-                    informListeners(PrivateMessage)
+                    informListeners(PrivateMessage, true)
                 }
 
                 is ConversationMessagesReadEvent -> {
                     countPrivateMessages.removeCount(e.contactId, e.count)
+                    informListeners(PrivateMessage, false)
                 }
 
                 is ForumPostReceivedEvent -> {
                     countForumPosts.add(e.groupId)
-                    informListeners(Forum)
+                    informListeners(Forum, true)
                 }
 
                 is ForumPostReadEvent -> {
                     countForumPosts.removeCount(e.groupId, e.numMarkedRead)
+                    informListeners(Forum, false)
                 }
             }
         }
@@ -99,12 +101,12 @@ constructor(
 
     override fun removeListener(listener: MessageCounterListener) = listeners.remove(listener)
 
-    private fun informListeners(type: MessageCounterDataType) = listeners.forEach { l ->
+    private fun informListeners(type: MessageCounterDataType, increment: Boolean) = listeners.forEach { l ->
         val groupCount = when (type) {
             PrivateMessage -> countPrivateMessages
             Forum -> countForumPosts
         }
-        l.invoke(MessageCounterData(type, groupCount.total, groupCount.unique))
+        l.invoke(MessageCounterData(type, groupCount.total, groupCount.unique, increment))
     }
 
     private fun <T> Multiset<T>.removeCount(t: T, count: Int) =