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 ce6dd4eedcaa23ed11b3ffda617fba92d2df9475..c2b3577ae2ce563baeb206a7429cb1950130c6b4 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt
@@ -1,5 +1,6 @@
 package org.briarproject.briar.desktop.contact
 
+import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.mutableStateOf
 import mu.KotlinLogging
 import org.briarproject.bramble.api.connection.ConnectionRegistry
@@ -43,7 +44,16 @@ constructor(
     private val _selectedContactId = mutableStateOf<ContactIdWrapper?>(null)
 
     val filterBy = _filterBy.asState()
-    val selectedContactId = _selectedContactId.asState()
+    val selectedContactId = derivedStateOf {
+        // reset selected contact to null if not part of list after filtering
+        val id = _selectedContactId.value
+        if (id == null || contactList.value.map { it.idWrapper }.contains(id)) {
+            id
+        } else {
+            _selectedContactId.value = null
+            null
+        }
+    }
 
     fun selectContact(contactItem: BaseContactItem) {
         _selectedContactId.value = contactItem.idWrapper
@@ -56,17 +66,6 @@ constructor(
 
     fun setFilterBy(filter: String) {
         _filterBy.value = filter
-        updateFilteredList()
-    }
-
-    override fun updateFilteredList() {
-        super.updateFilteredList()
-
-        // reset selected contact to null if not available after filtering
-        val id = _selectedContactId.value
-        if (id != null && !contactList.map { it.idWrapper }.contains(id)) {
-            _selectedContactId.value = null
-        }
     }
 
     override fun eventOccurred(e: Event?) {
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt
index 237fcf4001e870361deb9fd1b9b1ab40d8e892a2..0fcf16cd13ae79d49ccc032728c128604c0e39ee 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt
@@ -1,5 +1,6 @@
 package org.briarproject.briar.desktop.contact
 
+import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.mutableStateListOf
 import mu.KotlinLogging
 import org.briarproject.bramble.api.connection.ConnectionRegistry
@@ -36,9 +37,10 @@ abstract class ContactsViewModel(
     }
 
     private val _fullContactList = mutableStateListOf<BaseContactItem>()
-    private val _filteredContactList = mutableStateListOf<BaseContactItem>()
 
-    val contactList: List<BaseContactItem> = _filteredContactList
+    val contactList = derivedStateOf {
+        _fullContactList.filter(::filterContactItem).sortedByDescending { it.timestamp }
+    }
 
     protected open fun filterContactItem(contactItem: BaseContactItem) = true
 
@@ -61,18 +63,10 @@ abstract class ContactsViewModel(
             )
             txn.attach {
                 _fullContactList.clearAndAddAll(contactList)
-                updateFilteredList()
             }
         }
     }
 
-    // todo: when migrated to StateFlow, this could be done implicitly instead
-    protected open fun updateFilteredList() {
-        _filteredContactList.clearAndAddAll(
-            _fullContactList.filter(::filterContactItem).sortedByDescending { it.timestamp }
-        )
-    }
-
     override fun eventOccurred(e: Event?) {
         when (e) {
             is ContactAddedEvent -> {
@@ -105,13 +99,11 @@ abstract class ContactsViewModel(
             { it.idWrapper.contactId == contactId },
             update
         )
-        updateFilteredList()
     }
 
     protected open fun removeItem(contactId: ContactId) {
         _fullContactList.removeFirst<BaseContactItem, ContactItem> {
             it.idWrapper.contactId == contactId
         }
-        updateFilteredList()
     }
 }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageScreen.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageScreen.kt
index fe336200cc5ca5027d7887f4e1cf56af675ebb1e..4f254fa88d343124836258b0e1cfe4614d0b7b97 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageScreen.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageScreen.kt
@@ -19,7 +19,7 @@ fun PrivateMessageScreen(
 ) {
     Row(modifier = Modifier.fillMaxWidth()) {
         ContactList(
-            viewModel.contactList,
+            viewModel.contactList.value,
             viewModel::isSelected,
             viewModel::selectContact,
             viewModel.filterBy.value,
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/introduction/ContactDrawerMakeIntro.kt b/src/main/kotlin/org/briarproject/briar/desktop/introduction/ContactDrawerMakeIntro.kt
index 1f63c8d9aa5761c7cbd8500d0a76ed4c135285aa..f74bec81a797ccd656b74c3cc98c1473767f6377 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/introduction/ContactDrawerMakeIntro.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/introduction/ContactDrawerMakeIntro.kt
@@ -63,7 +63,7 @@ fun ContactDrawerMakeIntro(
                 }
                 HorizontalDivider()
                 LazyColumn {
-                    items(viewModel.contactList) { contactItem ->
+                    items(viewModel.contactList.value) { contactItem ->
                         if (contactItem is ContactItem)
                             ContactCard(
                                 contactItem,