diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItem.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItem.kt
index 62ebc584ecb602dde7bd4478d65345c8fc6f51a5..2ac68bc97de1277ad9e5176b913330398b2cb6a3 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItem.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItem.kt
@@ -45,4 +45,7 @@ data class ContactItem(
     fun updateAlias(a: String?): ContactItem {
         return copy(alias = a)
     }
+
+    fun updateFromMessagesRead(c: Int): ContactItem =
+        copy(unread = unread - c)
 }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt
index 254ce8fe4ed9f48d0c2fb13f3db319de203b41e1..cf26aea936405ab5111b443068d6462939197ceb 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt
@@ -11,6 +11,7 @@ import org.briarproject.bramble.api.event.Event
 import org.briarproject.bramble.api.event.EventBus
 import org.briarproject.briar.api.conversation.ConversationManager
 import org.briarproject.briar.api.conversation.event.ConversationMessageTrackedEvent
+import org.briarproject.briar.desktop.conversation.ConversationMessagesReadEvent
 import javax.inject.Inject
 
 class ContactListViewModel
@@ -72,6 +73,10 @@ constructor(
             is ContactAliasChangedEvent -> {
                 updateItem(e.contactId) { it.updateAlias(e.alias) }
             }
+            is ConversationMessagesReadEvent -> {
+                LOG.info("${e.count} conversation messages read, updating item")
+                updateItem(e.contactId) { it.updateFromMessagesRead(e.count) }
+            }
         }
     }
 }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationMessagesReadEvent.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationMessagesReadEvent.kt
new file mode 100644
index 0000000000000000000000000000000000000000..659067a0c401cc41b83809fbfef640c4b32f01ff
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationMessagesReadEvent.kt
@@ -0,0 +1,13 @@
+package org.briarproject.briar.desktop.conversation
+
+import org.briarproject.bramble.api.contact.ContactId
+import org.briarproject.bramble.api.event.Event
+
+/**
+ * An event that is broadcast when conversation messages
+ * are shown on the screen for the first time.
+ */
+data class ConversationMessagesReadEvent(
+    val count: Int,
+    val contactId: ContactId
+) : Event()
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationScreen.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationScreen.kt
index 8390eb90a048f227da1e3020f47c7d32036d24d8..0b3d1898ba2b9dda9929667a4fc7b245ac1cfe73 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationScreen.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationScreen.kt
@@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.requiredSize
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.MaterialTheme
 import androidx.compose.material.Scaffold
@@ -55,6 +56,8 @@ fun ConversationScreen(
 
     val (infoDrawer, setInfoDrawer) = remember { mutableStateOf(false) }
     val (contactDrawerState, setDrawerState) = remember { mutableStateOf(ContactInfoDrawerState.MakeIntro) }
+    val scrollState = rememberLazyListState()
+
     BoxWithConstraints(Modifier.fillMaxSize()) {
         val animatedInfoDrawerOffsetX by animateDpAsState(if (infoDrawer) (-275).dp else 0.dp)
         Scaffold(
@@ -69,6 +72,7 @@ fun ConversationScreen(
             content = { padding ->
                 LazyColumn(
                     verticalArrangement = Arrangement.spacedBy(8.dp),
+                    state = scrollState,
                     // reverseLayout to display most recent message (index 0) at the bottom
                     reverseLayout = true,
                     contentPadding = PaddingValues(8.dp),
@@ -88,6 +92,14 @@ fun ConversationScreen(
                 )
             },
         )
+
+        if (viewModel.hasUnreadMessages.value) {
+            LaunchedEffect(scrollState.firstVisibleItemIndex) {
+                // mark all messages older than the first visible item as read
+                viewModel.markMessagesRead(scrollState.firstVisibleItemIndex)
+            }
+        }
+
         if (infoDrawer) {
             // TODO Find non-hacky way of setting scrim on entire app
             Box(
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt
index affc255f9a9a4a2b55502f5ac0cbd898d9f687db..79e76b2c13219fd237d0c5af753091aa839a2908 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt
@@ -1,6 +1,7 @@
 package org.briarproject.briar.desktop.conversation
 
 import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.mutableStateListOf
 import androidx.compose.runtime.mutableStateOf
 import mu.KotlinLogging
@@ -31,6 +32,7 @@ import org.briarproject.briar.api.messaging.PrivateMessageHeader
 import org.briarproject.briar.desktop.contact.ContactItem
 import org.briarproject.briar.desktop.utils.KLoggerUtils.logDuration
 import org.briarproject.briar.desktop.utils.replaceIf
+import org.briarproject.briar.desktop.utils.replaceIfIndexed
 import org.briarproject.briar.desktop.viewmodel.BriarEventListenerViewModel
 import java.util.Date
 import javax.inject.Inject
@@ -108,6 +110,18 @@ constructor(
         }
     }
 
+    val hasUnreadMessages = derivedStateOf { _messages.any { !it.isRead } }
+
+    fun markMessagesRead(untilIndex: Int) {
+        var count = 0
+        _messages.replaceIfIndexed({ idx, it -> idx >= untilIndex && !it.isRead }) { _, it ->
+            conversationManager.setReadFlag(it.groupId, it.id, true)
+            count++
+            it.markRead()
+        }
+        eventBus.broadcast(ConversationMessagesReadEvent(count, contactItem.value!!.contactId))
+    }
+
     @Throws(DbException::class)
     private fun createMessage(text: String): PrivateMessage {
         val groupId = messagingManager.getConversationId(_contactItem.value!!.contactId)
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/utils/ListUtils.kt b/src/main/kotlin/org/briarproject/briar/desktop/utils/ListUtils.kt
index b2221c8a5275f3e4e375fb438c4677fc04111744..5f4b3cd60976c37d7bb95ecfcf8bef46ce72024e 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/utils/ListUtils.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/utils/ListUtils.kt
@@ -10,6 +10,18 @@ fun <T> MutableList<T>.replaceIf(predicate: (T) -> Boolean, transformation: (T)
     }
 }
 
+fun <T> MutableList<T>.replaceIfIndexed(predicate: (Int, T) -> Boolean, transformation: (Int, T) -> T) {
+    val li = listIterator()
+    var index = 0
+    while (li.hasNext()) {
+        val n = li.next()
+        if (predicate(index, n)) {
+            li.set(transformation(index, n))
+        }
+        index++
+    }
+}
+
 fun <T> MutableList<T>.replaceFirst(predicate: (T) -> Boolean, transformation: (T) -> T) {
     val li = listIterator()
     while (li.hasNext()) {