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 f380fea9d45859e40d2dd81c93e54759590e4aff..e89cedcc1e806170e2023924e3e093325b0d5bd2 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt
@@ -19,19 +19,17 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.drawscope.withTransform
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
-import org.briarproject.bramble.api.contact.Contact
 import org.briarproject.briar.desktop.theme.outline
 import org.briarproject.briar.desktop.theme.selectedCard
 import org.briarproject.briar.desktop.theme.surfaceVariant
-import org.briarproject.briar.desktop.ui.CVM
 import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE
 import org.briarproject.briar.desktop.ui.HorizontalDivider
 import org.briarproject.briar.desktop.utils.TimeUtils.getFormattedTimestamp
 
 @Composable
 fun ContactCard(
-    contact: Contact,
-    onSel: (Contact) -> Unit,
+    contactItem: ContactItem,
+    onSel: () -> Unit,
     selected: Boolean,
     drawNotifications: Boolean
 ) {
@@ -42,7 +40,7 @@ fun ContactCard(
     val briarSurfaceVar = MaterialTheme.colors.surfaceVariant
 
     Card(
-        modifier = Modifier.fillMaxWidth().height(HEADER_SIZE).clickable(onClick = { onSel(contact) }),
+        modifier = Modifier.fillMaxWidth().height(HEADER_SIZE).clickable(onClick = onSel),
         shape = RoundedCornerShape(0.dp),
         backgroundColor = bgColor,
         contentColor = MaterialTheme.colors.onSurface
@@ -50,7 +48,7 @@ fun ContactCard(
         Row(horizontalArrangement = Arrangement.SpaceBetween) {
             Row(modifier = Modifier.align(Alignment.CenterVertically).padding(horizontal = 16.dp)) {
                 // TODO Pull profile pictures
-                ProfileCircle(36.dp, contact.author.id.bytes)
+                ProfileCircle(36.dp, contactItem.contact.author.id.bytes)
                 // Draw notification badges
                 if (drawNotifications) {
                     Canvas(
@@ -66,13 +64,12 @@ fun ContactCard(
                 }
                 Column(modifier = Modifier.align(Alignment.CenterVertically).padding(start = 12.dp)) {
                     Text(
-                        contact.author.name,
+                        contactItem.contact.author.name,
                         fontSize = 14.sp,
                         modifier = Modifier.align(Alignment.Start).padding(bottom = 2.dp)
                     )
-                    val latestMsgTime = CVM.current.getGroupCount(contact.id).latestMsgTime
                     Text(
-                        getFormattedTimestamp(latestMsgTime),
+                        if (contactItem.isEmpty) "No messages." else getFormattedTimestamp(contactItem.timestamp),
                         fontSize = 10.sp,
                         modifier = Modifier.align(Alignment.Start)
                     )
@@ -81,14 +78,12 @@ fun ContactCard(
             Canvas(
                 modifier = Modifier.padding(start = 32.dp, end = 18.dp).size(22.dp).align(Alignment.CenterVertically),
                 onDraw = {
-                    val size = 16.dp.toPx()
-                    drawCircle(color = outlineColor, radius = size / 2f)
-                    // TODO check if contact online
-                    if (true) {
-                        drawCircle(color = briarSecondary, radius = 14.dp.toPx() / 2f)
-                    } else {
-                        drawCircle(color = briarSurfaceVar, radius = 14.dp.toPx() / 2f)
-                    }
+                    val size = 16.dp
+                    drawCircle(color = outlineColor, radius = size.toPx() / 2f)
+                    drawCircle(
+                        color = if (contactItem.isConnected) briarSecondary else briarSurfaceVar,
+                        radius = (size - 2.dp).toPx() / 2f
+                    )
                 }
             )
         }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDrawerMakeIntro.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDrawerMakeIntro.kt
index d0fc82fb2e26734d115c5ea87701893974ce0057..76e4c31547f7f835257621f1920b2ddd015d7a82 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDrawerMakeIntro.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDrawerMakeIntro.kt
@@ -57,7 +57,8 @@ fun ContactDrawerMakeIntro(contact: Contact, contacts: List<Contact>, setInfoDra
             Column(Modifier.verticalScroll(rememberScrollState())) {
                 for (c in contacts) {
                     if (c.id != contact.id) {
-                        ContactCard(c, { onCancelSel(c); introNextPg = true }, false, false)
+                        // todo: refactor to use contactItem in IntroductionViewModel
+                        //ContactCard(c, { onCancelSel(c); introNextPg = true }, false, false)
                     }
                 }
             }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItem.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItem.kt
new file mode 100644
index 0000000000000000000000000000000000000000..df2a19806f06e2b0ab2bff21385ecce9fac0da2b
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItem.kt
@@ -0,0 +1,16 @@
+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.identity.AuthorInfo
+
+data class ContactItem(
+    val contact: Contact,
+    private val authorInfo: AuthorInfo,
+    val isConnected: Boolean,
+    private val groupCount: MessageTracker.GroupCount
+) {
+    val isEmpty = groupCount.msgCount == 0
+    val unread = groupCount.unreadCount
+    val timestamp = groupCount.latestMsgTime
+}
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 528a52b6967bf73f272cf7b5396c6f35b6e54de9..345e3f80b4bb003099c0c49fc4aa20bc7e357d2e 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt
@@ -37,8 +37,8 @@ fun ContactList(
         },
         content = {
             LazyColumn {
-                itemsIndexed(viewModel.contactList) { index, contact ->
-                    ContactCard(contact, { viewModel.selectContact(index) }, viewModel.isSelected(index), true)
+                itemsIndexed(viewModel.contactList) { index, contactItem ->
+                    ContactCard(contactItem, { viewModel.selectContact(index) }, viewModel.isSelected(index), true)
                 }
             }
         },
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 b653272c29d7223b14fb0ab321b8fa22ed155797..01235ac0fd47bb96ee6bc4f516691565394786c7 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt
@@ -4,12 +4,14 @@ import androidx.compose.runtime.State
 import androidx.compose.runtime.mutableStateListOf
 import androidx.compose.runtime.mutableStateOf
 import org.briarproject.bramble.api.FormatException
-import org.briarproject.bramble.api.contact.Contact
+import org.briarproject.bramble.api.connection.ConnectionRegistry
 import org.briarproject.bramble.api.contact.ContactManager
 import org.briarproject.bramble.api.db.ContactExistsException
 import org.briarproject.bramble.api.db.PendingContactExistsException
 import org.briarproject.bramble.api.identity.AuthorConstants
 import org.briarproject.bramble.util.StringUtils
+import org.briarproject.briar.api.conversation.ConversationManager
+import org.briarproject.briar.api.identity.AuthorManager
 import java.security.GeneralSecurityException
 import java.util.logging.Logger
 import javax.inject.Inject
@@ -18,37 +20,48 @@ class ContactsViewModel
 @Inject
 constructor(
     private val contactManager: ContactManager,
+    private val authorManager: AuthorManager,
+    private val conversationManager: ConversationManager,
+    private val connectionRegistry: ConnectionRegistry,
 ) {
 
     companion object {
         private val LOG = Logger.getLogger(ContactsViewModel::class.java.name)
     }
 
-    private val _contactList = mutableListOf<Contact>()
-    private val _filteredContactList = mutableStateListOf<Contact>()
+    private val _contactList = mutableListOf<ContactItem>()
+    private val _filteredContactList = mutableStateListOf<ContactItem>()
     private val _filterBy = mutableStateOf("")
     private var _selectedContactIndex = -1;
-    private val _selectedContact = mutableStateOf<Contact?>(null)
+    private val _selectedContact = mutableStateOf<ContactItem?>(null)
 
     private val _addContactDialogVisible = mutableStateOf(false)
     private val _addContactAlias = mutableStateOf("")
     private val _addContactLink = mutableStateOf("")
 
-    val contactList: List<Contact> = _filteredContactList
+    val contactList: List<ContactItem> = _filteredContactList
     val filterBy: State<String> = _filterBy
-    val selectedContact: State<Contact?> = _selectedContact
+    val selectedContact: State<ContactItem?> = _selectedContact
 
     val addContactDialogVisible: State<Boolean> = _addContactDialogVisible
     val addContactAlias: State<String> = _addContactAlias
     val addContactLink: State<String> = _addContactLink
-    val addContactOwnLink = contactManager.handshakeLink
+    var addContactOwnLink = ""
 
     internal fun loadContacts() {
         _contactList.apply {
             clear()
-            addAll(contactManager.contacts)
+            addAll(contactManager.contacts.map { contact ->
+                ContactItem(
+                    contact,
+                    authorManager.getAuthorInfo(contact),
+                    connectionRegistry.isConnected(contact.id),
+                    conversationManager.getGroupCount(contact.id)
+                )
+            })
         }
         updateFilteredList()
+        addContactOwnLink = contactManager.handshakeLink
     }
 
     fun selectContact(index: Int) {
@@ -63,7 +76,7 @@ constructor(
             clear()
             addAll(_contactList.filter {
                 // todo: also filter on alias?
-                it.author.name.lowercase().contains(_filterBy.value)
+                it.contact.author.name.lowercase().contains(_filterBy.value)
             })
         }
     }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageView.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageView.kt
index d82bc21fcecb0154159fa866b545ab4fde131374..48370f4fbf2ac77904e0fc9d7c646dbc8fb94a55 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageView.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageView.kt
@@ -27,8 +27,8 @@ fun PrivateMessageView(
         Column(modifier = Modifier.weight(1f).fillMaxHeight()) {
             contacts.selectedContact.value?.also { selectedContact ->
                 Conversation(
-                    selectedContact,
-                    contacts.contactList,
+                    selectedContact.contact,
+                    contacts.contactList.map { c -> c.contact },
                     dropdownExpanded,
                     setExpanded,
                     infoDrawer,