diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt
index e89cedcc1e806170e2023924e3e093325b0d5bd2..3a15dc7a06173669c4c87aabbf9a4ef7d6a9a68c 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt
@@ -31,7 +31,6 @@ fun ContactCard(
     contactItem: ContactItem,
     onSel: () -> Unit,
     selected: Boolean,
-    drawNotifications: Boolean
 ) {
     val bgColor = if (selected) MaterialTheme.colors.selectedCard else MaterialTheme.colors.surfaceVariant
     val outlineColor = MaterialTheme.colors.outline
@@ -50,7 +49,7 @@ fun ContactCard(
                 // TODO Pull profile pictures
                 ProfileCircle(36.dp, contactItem.contact.author.id.bytes)
                 // Draw notification badges
-                if (drawNotifications) {
+                if (contactItem.unread > 0) {
                     Canvas(
                         modifier = Modifier.align(Alignment.CenterVertically),
                         onDraw = {
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 df2a19806f06e2b0ab2bff21385ecce9fac0da2b..10c12cbdce721addc9455b316ecf766bb6d54b83 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItem.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItem.kt
@@ -2,15 +2,37 @@ package org.briarproject.briar.desktop.contact
 
 import org.briarproject.bramble.api.contact.Contact
 import org.briarproject.briar.api.client.MessageTracker
+import org.briarproject.briar.api.conversation.ConversationMessageHeader
 import org.briarproject.briar.api.identity.AuthorInfo
+import kotlin.math.max
 
 data class ContactItem(
     val contact: Contact,
     private val authorInfo: AuthorInfo,
+    private val groupCount: MessageTracker.GroupCount,
+
     val isConnected: Boolean,
-    private val groupCount: MessageTracker.GroupCount
+    val isEmpty: Boolean = groupCount.msgCount == 0,
+    val unread: Int = groupCount.unreadCount,
+    val timestamp: Long = groupCount.latestMsgTime
 ) {
-    val isEmpty = groupCount.msgCount == 0
-    val unread = groupCount.unreadCount
-    val timestamp = groupCount.latestMsgTime
+    fun updateFromMessageHeader(h: ConversationMessageHeader): ContactItem {
+        return copy(
+            isEmpty = false,
+            unread = if (h.isRead) unread else unread + 1,
+            timestamp = max(h.timestamp, timestamp)
+        )
+    }
+
+    fun updateIsConnected(c: Boolean): ContactItem {
+        return copy(isConnected = c)
+    }
+
+    fun updateAlias(a: String?): ContactItem {
+        return copy(contact = contact.updateAlias(a))
+    }
+
+    private fun Contact.updateAlias(a: String?): Contact {
+        return Contact(id, author, localAuthorId, a, handshakePublicKey, isVerified)
+    }
 }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt
index 345e3f80b4bb003099c0c49fc4aa20bc7e357d2e..2c858b284bb6e7e4f77745a27e8632f2175cc57c 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt
@@ -38,7 +38,7 @@ fun ContactList(
         content = {
             LazyColumn {
                 itemsIndexed(viewModel.contactList) { index, contactItem ->
-                    ContactCard(contactItem, { viewModel.selectContact(index) }, viewModel.isSelected(index), true)
+                    ContactCard(contactItem, { viewModel.selectContact(index) }, viewModel.isSelected(index))
                 }
             }
         },
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 01235ac0fd47bb96ee6bc4f516691565394786c7..a3a6fe7813f1ee790a146569702d59026aaddcc1 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt
@@ -5,13 +5,25 @@ import androidx.compose.runtime.mutableStateListOf
 import androidx.compose.runtime.mutableStateOf
 import org.briarproject.bramble.api.FormatException
 import org.briarproject.bramble.api.connection.ConnectionRegistry
+import org.briarproject.bramble.api.contact.ContactId
 import org.briarproject.bramble.api.contact.ContactManager
+import org.briarproject.bramble.api.contact.event.ContactAddedEvent
+import org.briarproject.bramble.api.contact.event.ContactAliasChangedEvent
+import org.briarproject.bramble.api.contact.event.ContactRemovedEvent
 import org.briarproject.bramble.api.db.ContactExistsException
 import org.briarproject.bramble.api.db.PendingContactExistsException
+import org.briarproject.bramble.api.event.Event
+import org.briarproject.bramble.api.event.EventBus
+import org.briarproject.bramble.api.event.EventListener
 import org.briarproject.bramble.api.identity.AuthorConstants
+import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent
+import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent
 import org.briarproject.bramble.util.StringUtils
 import org.briarproject.briar.api.conversation.ConversationManager
+import org.briarproject.briar.api.conversation.event.ConversationMessageReceivedEvent
 import org.briarproject.briar.api.identity.AuthorManager
+import org.briarproject.briar.desktop.utils.removeFirst
+import org.briarproject.briar.desktop.utils.replaceFirst
 import java.security.GeneralSecurityException
 import java.util.logging.Logger
 import javax.inject.Inject
@@ -23,12 +35,18 @@ constructor(
     private val authorManager: AuthorManager,
     private val conversationManager: ConversationManager,
     private val connectionRegistry: ConnectionRegistry,
-) {
+    private val eventBus: EventBus,
+) : EventListener {
 
     companion object {
         private val LOG = Logger.getLogger(ContactsViewModel::class.java.name)
     }
 
+    init {
+        //todo: where/when to remove listener again?
+        eventBus.addListener(this)
+    }
+
     private val _contactList = mutableListOf<ContactItem>()
     private val _filteredContactList = mutableStateListOf<ContactItem>()
     private val _filterBy = mutableStateOf("")
@@ -55,12 +73,13 @@ constructor(
                 ContactItem(
                     contact,
                     authorManager.getAuthorInfo(contact),
-                    connectionRegistry.isConnected(contact.id),
-                    conversationManager.getGroupCount(contact.id)
+                    conversationManager.getGroupCount(contact.id),
+                    connectionRegistry.isConnected(contact.id)
                 )
             })
         }
         updateFilteredList()
+        //todo: do in event instead?
         addContactOwnLink = contactManager.handshakeLink
     }
 
@@ -148,4 +167,43 @@ constructor(
         val aliasUtf8 = StringUtils.toUtf8(alias)
         return aliasUtf8.isEmpty() || aliasUtf8.size > AuthorConstants.MAX_AUTHOR_NAME_LENGTH
     }
+
+    override fun eventOccurred(e: Event?) {
+        when (e) {
+            is ContactAddedEvent -> {
+                LOG.info("Contact added, reloading")
+                loadContacts()
+            }
+            is ContactConnectedEvent -> {
+                LOG.info("Contact connected, update state")
+                updateItem(e.contactId) { it.updateIsConnected(true) }
+            }
+            is ContactDisconnectedEvent -> {
+                LOG.info("Contact disconnected, update state")
+                updateItem(e.contactId) { it.updateIsConnected(false) }
+            }
+            is ContactRemovedEvent -> {
+                LOG.info("Contact removed, removing item")
+                removeItem(e.contactId)
+            }
+            is ConversationMessageReceivedEvent<*> -> {
+                LOG.info("Conversation message received, updating item")
+                updateItem(e.contactId) { it.updateFromMessageHeader(e.messageHeader) }
+            }
+            //is AvatarUpdatedEvent -> {}
+            is ContactAliasChangedEvent -> {
+                updateItem(e.contactId) { it.updateAlias(e.alias) }
+            }
+        }
+    }
+
+    private fun updateItem(contactId: ContactId, update: (ContactItem) -> ContactItem) {
+        _contactList.replaceFirst({ it.contact.id == contactId }, update)
+        updateFilteredList()
+    }
+
+    private fun removeItem(contactId: ContactId) {
+        _contactList.removeFirst { it.contact.id == contactId }
+        updateFilteredList()
+    }
 }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/utils/ListUtils.kt b/src/main/kotlin/org/briarproject/briar/desktop/utils/ListUtils.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3adad7c3f2e0d7746102862e9cefa120c5e2740c
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/utils/ListUtils.kt
@@ -0,0 +1,23 @@
+package org.briarproject.briar.desktop.utils
+
+fun <T> MutableList<T>.replaceFirst(predicate: (T) -> Boolean, transformation: (T) -> T) {
+    val li = listIterator()
+    while (li.hasNext()) {
+        val n = li.next()
+        if (predicate(n)) {
+            li.set(transformation(n))
+            break
+        }
+    }
+}
+
+fun <T> MutableList<T>.removeFirst(predicate: (T) -> Boolean) {
+    val li = listIterator()
+    while (li.hasNext()) {
+        val n = li.next()
+        if (predicate(n)) {
+            li.remove()
+            break
+        }
+    }
+}