diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/AddContactDialog.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/AddContactDialog.kt
index 2d2f5ae5b0d2c8485a73f838a60de3ce0ce4f911..7a623d2e8d0e25875b92d2d7e767dc8a38845832 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/AddContactDialog.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/AddContactDialog.kt
@@ -15,35 +15,16 @@ import androidx.compose.material.Text
 import androidx.compose.material.TextButton
 import androidx.compose.material.TextField
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
-import org.briarproject.bramble.api.FormatException
-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.desktop.ui.CTM
-import java.security.GeneralSecurityException
 
 @OptIn(ExperimentalMaterialApi::class)
 @Composable
-fun AddContactDialog(isVisible: Boolean, setDialogVisibility: (Boolean) -> Unit) {
-    if (!isVisible) {
-        return
-    }
-    var contactAlias by remember { mutableStateOf("") }
-    var contactLink by remember { mutableStateOf("") }
-    val contactManager = CTM.current
-    val ownLink = CTM.current.handshakeLink
+fun AddContactDialog(viewModel: ContactsViewModel) {
     AlertDialog(
-        onDismissRequest = { setDialogVisibility(false) },
+        onDismissRequest = viewModel::closeAddContactDialog,
         text = {
             Column(modifier = Modifier.fillMaxWidth()) {
                 Row(Modifier.fillMaxWidth().padding(vertical = 16.dp)) {
@@ -58,14 +39,22 @@ fun AddContactDialog(isVisible: Boolean, setDialogVisibility: (Boolean) -> Unit)
                         "Contact's Link",
                         Modifier.width(128.dp).align(Alignment.CenterVertically),
                     )
-                    TextField(contactLink, onValueChange = { contactLink = it }, modifier = Modifier.fillMaxWidth())
+                    TextField(
+                        viewModel.addContactLink.value,
+                        viewModel::setAddContactLink,
+                        modifier = Modifier.fillMaxWidth()
+                    )
                 }
                 Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
                     Text(
                         "Contact's Name",
                         Modifier.width(128.dp).align(Alignment.CenterVertically),
                     )
-                    TextField(contactAlias, onValueChange = { contactAlias = it }, modifier = Modifier.fillMaxWidth())
+                    TextField(
+                        viewModel.addContactAlias.value,
+                        viewModel::setAddContactAlias,
+                        modifier = Modifier.fillMaxWidth()
+                    )
                 }
                 Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
                     Text(
@@ -73,7 +62,7 @@ fun AddContactDialog(isVisible: Boolean, setDialogVisibility: (Boolean) -> Unit)
                         modifier = Modifier.width(128.dp).align(Alignment.CenterVertically),
                     )
                     TextField(
-                        ownLink,
+                        viewModel.addContactOwnLink,
                         onValueChange = {},
                         modifier = Modifier.fillMaxWidth()
                     )
@@ -81,23 +70,13 @@ fun AddContactDialog(isVisible: Boolean, setDialogVisibility: (Boolean) -> Unit)
             }
         },
         confirmButton = {
-            Button(
-                onClick = {
-                    if (ownLink.equals(contactLink)) {
-                        println("Please enter contact's link, not your own")
-                        setDialogVisibility(false)
-                        return@Button
-                    }
-                    addPendingContact(contactManager, contactAlias, contactLink)
-                    setDialogVisibility(false)
-                },
-            ) {
+            Button(onClick = viewModel::onSubmitAddContactDialog) {
                 Text("Add")
             }
         },
         dismissButton = {
             TextButton(
-                onClick = { setDialogVisibility(false) }
+                onClick = viewModel::closeAddContactDialog
             ) {
                 Text("Cancel", color = MaterialTheme.colors.onSurface)
             }
@@ -105,38 +84,3 @@ fun AddContactDialog(isVisible: Boolean, setDialogVisibility: (Boolean) -> Unit)
         modifier = Modifier.size(600.dp, 300.dp),
     )
 }
-
-private fun addPendingContact(contactManager: ContactManager, alias: String, link: String) {
-    if (aliasIsInvalid(alias)) {
-        println("Alias is invalid")
-        return
-    }
-    try {
-        contactManager.addPendingContact(link, alias)
-    } catch (e: FormatException) {
-        println("Link is invalid")
-        println(e.stackTrace)
-    } catch (e: GeneralSecurityException) {
-        println("Public key is invalid")
-        println(e.stackTrace)
-    }
-    /*
-    TODO: Warn user that the following two errors might be an attack
-
-     Use `e.pendingContact.id.bytes` and `e.pendingContact.alias` to implement the following logic:
-     https://code.briarproject.org/briar/briar-gtk/-/merge_requests/97
-
-    */
-    catch (e: ContactExistsException) {
-        println("Contact already exists")
-        println(e.stackTrace)
-    } catch (e: PendingContactExistsException) {
-        println("Pending Contact already exists")
-        println(e.stackTrace)
-    }
-}
-
-private fun aliasIsInvalid(alias: String): Boolean {
-    val aliasUtf8 = StringUtils.toUtf8(alias)
-    return aliasUtf8.isEmpty() || aliasUtf8.size > AuthorConstants.MAX_AUTHOR_NAME_LENGTH
-}
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 42edcbf1a70fcb48875265490f5a92655992a354..528a52b6967bf73f272cf7b5396c6f35b6e54de9 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt
@@ -18,9 +18,9 @@ import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE
 
 @Composable
 fun ContactList(
-    contacts: ContactsViewModel,
-    onContactAdd: () -> Unit,
+    viewModel: ContactsViewModel,
 ) {
+    if (viewModel.addContactDialogVisible.value) AddContactDialog(viewModel)
     Scaffold(
         modifier = Modifier.fillMaxHeight().width(CONTACTLIST_WIDTH),
         backgroundColor = MaterialTheme.colors.surfaceVariant,
@@ -29,16 +29,16 @@ fun ContactList(
                 modifier = Modifier.fillMaxWidth().height(HEADER_SIZE + 1.dp),
             ) {
                 SearchTextField(
-                    contacts.filterBy.value,
-                    onValueChange = contacts::setFilterBy,
-                    onContactAdd
+                    viewModel.filterBy.value,
+                    onValueChange = viewModel::setFilterBy,
+                    onContactAdd = viewModel::openAddContactDialog
                 )
             }
         },
         content = {
             LazyColumn {
-                itemsIndexed(contacts.contactList) { index, contact ->
-                    ContactCard(contact, { contacts.selectContact(index) }, contacts.isSelected(index), true)
+                itemsIndexed(viewModel.contactList) { index, contact ->
+                    ContactCard(contact, { 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 9bc56f9778384b0006acbb55c087eb4194bf8141..b653272c29d7223b14fb0ab321b8fa22ed155797 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt
@@ -3,8 +3,14 @@ package org.briarproject.briar.desktop.contact
 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.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 java.security.GeneralSecurityException
 import java.util.logging.Logger
 import javax.inject.Inject
 
@@ -24,10 +30,19 @@ constructor(
     private var _selectedContactIndex = -1;
     private val _selectedContact = mutableStateOf<Contact?>(null)
 
+    private val _addContactDialogVisible = mutableStateOf(false)
+    private val _addContactAlias = mutableStateOf("")
+    private val _addContactLink = mutableStateOf("")
+
     val contactList: List<Contact> = _filteredContactList
     val filterBy: State<String> = _filterBy
     val selectedContact: State<Contact?> = _selectedContact
 
+    val addContactDialogVisible: State<Boolean> = _addContactDialogVisible
+    val addContactAlias: State<String> = _addContactAlias
+    val addContactLink: State<String> = _addContactLink
+    val addContactOwnLink = contactManager.handshakeLink
+
     internal fun loadContacts() {
         _contactList.apply {
             clear()
@@ -57,4 +72,67 @@ constructor(
         _filterBy.value = filter
         updateFilteredList()
     }
+
+    fun openAddContactDialog() {
+        _addContactDialogVisible.value = true
+    }
+
+    fun closeAddContactDialog() {
+        _addContactDialogVisible.value = false
+    }
+
+    fun setAddContactAlias(alias: String) {
+        _addContactAlias.value = alias
+    }
+
+    fun setAddContactLink(link: String) {
+        _addContactLink.value = link
+    }
+
+    fun onSubmitAddContactDialog() {
+        val link = _addContactLink.value
+        val alias = _addContactAlias.value
+        addPendingContact(link, alias)
+        closeAddContactDialog()
+    }
+
+    private fun addPendingContact(link: String, alias: String) {
+        if (addContactOwnLink.equals(link)) {
+            println("Please enter contact's link, not your own")
+            return
+        }
+        if (aliasIsInvalid(alias)) {
+            println("Alias is invalid")
+            return
+        }
+
+        try {
+            contactManager.addPendingContact(link, alias)
+        } catch (e: FormatException) {
+            println("Link is invalid")
+            println(e.stackTrace)
+        } catch (e: GeneralSecurityException) {
+            println("Public key is invalid")
+            println(e.stackTrace)
+        }
+        /*
+        TODO: Warn user that the following two errors might be an attack
+
+         Use `e.pendingContact.id.bytes` and `e.pendingContact.alias` to implement the following logic:
+         https://code.briarproject.org/briar/briar-gtk/-/merge_requests/97
+
+        */
+        catch (e: ContactExistsException) {
+            println("Contact already exists")
+            println(e.stackTrace)
+        } catch (e: PendingContactExistsException) {
+            println("Pending Contact already exists")
+            println(e.stackTrace)
+        }
+    }
+
+    private fun aliasIsInvalid(alias: String): Boolean {
+        val aliasUtf8 = StringUtils.toUtf8(alias)
+        return aliasUtf8.isEmpty() || aliasUtf8.size > AuthorConstants.MAX_AUTHOR_NAME_LENGTH
+    }
 }
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 74b89783592ba6dec3e65c948e174c05d59ab001..4b30eddff7f738067cd626062b149ea8261bce00 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageView.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageView.kt
@@ -8,7 +8,6 @@ import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
-import org.briarproject.briar.desktop.contact.AddContactDialog
 import org.briarproject.briar.desktop.contact.ContactInfoDrawerState.MakeIntro
 import org.briarproject.briar.desktop.contact.ContactList
 import org.briarproject.briar.desktop.contact.ContactsViewModel
@@ -18,13 +17,11 @@ import org.briarproject.briar.desktop.ui.VerticalDivider
 fun PrivateMessageView(
     contacts: ContactsViewModel,
 ) {
-    val (isDialogVisible, setDialogVisibility) = remember { mutableStateOf(false) }
     val (dropdownExpanded, setExpanded) = remember { mutableStateOf(false) }
     val (infoDrawer, setInfoDrawer) = remember { mutableStateOf(false) }
     val (contactDrawerState, setDrawerState) = remember { mutableStateOf(MakeIntro) }
-    AddContactDialog(isDialogVisible, setDialogVisibility)
     Row(modifier = Modifier.fillMaxWidth()) {
-        ContactList(contacts) { setDialogVisibility(true) }
+        ContactList(contacts)
         VerticalDivider()
         Column(modifier = Modifier.weight(1f).fillMaxHeight()) {
             contacts.selectedContact.value?.let { selectedContact ->