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 857f736f317ee56fdf61c63820b5930490cd0965..1b0cae45c3323ff9e24e3b9a0dc855d98646b8d5 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt
@@ -64,14 +64,6 @@ constructor(
         private val LOG = KotlinLogging.logger {}
     }
 
-    private val conversationVisitor = derivedStateOf {
-        val c = _contactItem.value
-        if (c != null)
-            ConversationVisitor(c.name, ::loadMessageText)
-        else
-            null
-    }
-
     private val _contactId = mutableStateOf<ContactId?>(null)
     private val _contactItem = mutableStateOf<ContactItem?>(null)
     private val _messages = mutableStateListOf<ConversationItem>()
@@ -91,8 +83,10 @@ constructor(
         _contactItem.value = null
         _messages.clear()
 
-        loadContact(id)
-        loadMessages(id)
+        runOnDbThreadWithTransaction(true) { txn ->
+            val contact = loadContact(txn, id)
+            loadMessages(txn, contact)
+        }
 
         setNewMessage("")
     }
@@ -125,9 +119,9 @@ constructor(
                     m.hasText(), m.attachmentHeaders,
                     m.autoDeleteTimer
                 )
-                txn.attach {
-                    _messages.add(0, h.accept(conversationVisitor.value)!!)
-                }
+                val visitor = ConversationVisitor(contactItem.value!!.name, messagingManager, txn)
+                val msg = h.accept(visitor)!!
+                txn.attach { _messages.add(0, msg) }
             } catch (e: UnexpectedTimerException) {
                 // todo: handle this properly
                 LOG.warn(e) {}
@@ -170,7 +164,7 @@ constructor(
         }
     }
 
-    private fun loadContact(id: ContactId) = runOnDbThreadWithTransaction(true) { txn ->
+    private fun loadContact(txn: Transaction, id: ContactId): ContactItem {
         try {
             val start = LogUtils.now()
             val contactItem = ContactItem(
@@ -180,21 +174,24 @@ constructor(
             )
             LOG.logDuration("Loading contact", start)
             txn.attach { _contactItem.value = contactItem }
+            return contactItem
         } catch (e: NoSuchContactException) {
             // todo: handle this properly
             LOG.warn(e) {}
+            throw e
         }
     }
 
-    private fun loadMessages(id: ContactId) = runOnDbThreadWithTransaction(true) { txn ->
+    private fun loadMessages(txn: Transaction, contact: ContactItem) {
         try {
             var start = LogUtils.now()
-            val headers = conversationManager.getMessageHeaders(txn, id)
+            val headers = conversationManager.getMessageHeaders(txn, contact.idWrapper.contactId)
             LOG.logDuration("Loading message headers", start)
             // Sort headers by timestamp in *descending* order
             val sorted = headers.sortedByDescending { it.timestamp }
             start = LogUtils.now()
-            val messages = sorted.map { h -> h.accept(conversationVisitor.value)!! }
+            val visitor = ConversationVisitor(contact.name, messagingManager, txn)
+            val messages = sorted.map { h -> h.accept(visitor)!! }
             LOG.logDuration("Loading messages", start)
             txn.attach { _messages.clearAndAddAll(messages) }
         } catch (e: NoSuchContactException) {
@@ -203,15 +200,6 @@ constructor(
         }
     }
 
-    private fun loadMessageText(m: MessageId): String? {
-        try {
-            return messagingManager.getMessageText(m) // todo: use transactional API call somehow
-        } catch (e: DbException) {
-            LOG.warn(e) {}
-        }
-        return null
-    }
-
     override fun eventOccurred(e: Event?) {
         when (e) {
             is ContactRemovedEvent -> {
@@ -225,7 +213,11 @@ constructor(
                     LOG.info("Message received, adding")
                     val h = e.messageHeader
                     // insert at start of list according to descending sort order
-                    _messages.add(0, h.accept(conversationVisitor.value)!!)
+                    runOnDbThreadWithTransaction(true) { txn ->
+                        val visitor = ConversationVisitor(contactItem.value!!.name, messagingManager, txn)
+                        val msg = h.accept(visitor)!!
+                        txn.attach { _messages.add(0, msg) }
+                    }
                 }
             }
             is MessagesSentEvent -> {
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationVisitor.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationVisitor.kt
index f266b00cfb1ff4f0e484a60c6e9b6ae9ae487cce..d491cf6f148c81563307148fd3659a5525154233 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationVisitor.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationVisitor.kt
@@ -2,6 +2,7 @@ package org.briarproject.briar.desktop.conversation
 
 import mu.KotlinLogging
 import org.briarproject.bramble.api.db.DatabaseExecutor
+import org.briarproject.bramble.api.db.DbException
 import org.briarproject.bramble.api.db.Transaction
 import org.briarproject.bramble.api.sync.MessageId
 import org.briarproject.briar.api.blog.BlogInvitationRequest
@@ -11,6 +12,7 @@ import org.briarproject.briar.api.forum.ForumInvitationRequest
 import org.briarproject.briar.api.forum.ForumInvitationResponse
 import org.briarproject.briar.api.introduction.IntroductionRequest
 import org.briarproject.briar.api.introduction.IntroductionResponse
+import org.briarproject.briar.api.messaging.MessagingManager
 import org.briarproject.briar.api.messaging.PrivateMessageHeader
 import org.briarproject.briar.api.privategroup.invitation.GroupInvitationRequest
 import org.briarproject.briar.api.privategroup.invitation.GroupInvitationResponse
@@ -20,18 +22,29 @@ import org.briarproject.briar.desktop.utils.UiUtils.getContactDisplayName
 
 internal class ConversationVisitor(
     private val contactName: String,
-    private val loadMessageText: (MessageId) -> String?
+    private val messagingManager: MessagingManager,
+    private val txn: Transaction,
 ) : ConversationMessageVisitor<ConversationItem?> {
 
     companion object {
         private val LOG = KotlinLogging.logger {}
     }
 
+    // todo: implement some message cache similar to Briar Android
+    private fun loadMessageText(txn: Transaction, m: MessageId): String? {
+        try {
+            return messagingManager.getMessageText(txn, m)
+        } catch (e: DbException) {
+            LOG.warn(e) {}
+        }
+        return null
+    }
+
     @DatabaseExecutor
     override fun visitPrivateMessageHeader(h: PrivateMessageHeader): ConversationItem {
         val item = ConversationMessageItem(h)
         if (h.hasText()) {
-            item.text = loadMessageText(h.id)
+            item.text = loadMessageText(txn, h.id)
         } else {
             LOG.warn { "private message without text" }
         }