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 2c858b284bb6e7e4f77745a27e8632f2175cc57c..7fde6fe404e140b6f41e4e285f8fb34d0dfcff9a 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt
@@ -10,8 +10,14 @@ import androidx.compose.foundation.lazy.itemsIndexed
 import androidx.compose.material.MaterialTheme
 import androidx.compose.material.Scaffold
 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.Modifier
 import androidx.compose.ui.unit.dp
+import org.briarproject.briar.desktop.contact.add.remote.AddContactDialog
+import org.briarproject.briar.desktop.contact.add.remote.AddContactViewModel
 import org.briarproject.briar.desktop.theme.surfaceVariant
 import org.briarproject.briar.desktop.ui.Constants.CONTACTLIST_WIDTH
 import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE
@@ -19,8 +25,10 @@ import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE
 @Composable
 fun ContactList(
     viewModel: ContactsViewModel,
+    addContactViewModel: AddContactViewModel,
 ) {
-    if (viewModel.addContactDialogVisible.value) AddContactDialog(viewModel)
+    var isContactDialogVisible by remember { mutableStateOf(false) }
+    if (isContactDialogVisible) AddContactDialog(addContactViewModel) { isContactDialogVisible = false }
     Scaffold(
         modifier = Modifier.fillMaxHeight().width(CONTACTLIST_WIDTH),
         backgroundColor = MaterialTheme.colors.surfaceVariant,
@@ -31,7 +39,7 @@ fun ContactList(
                 SearchTextField(
                     viewModel.filterBy.value,
                     onValueChange = viewModel::setFilterBy,
-                    onContactAdd = viewModel::openAddContactDialog
+                    onContactAdd = { isContactDialogVisible = 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 a3a6fe7813f1ee790a146569702d59026aaddcc1..32da44de5078d3351a54c97cea358fab413be821 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt
@@ -3,28 +3,22 @@ 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.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
 
@@ -53,19 +47,10 @@ constructor(
     private var _selectedContactIndex = -1;
     private val _selectedContact = mutableStateOf<ContactItem?>(null)
 
-    private val _addContactDialogVisible = mutableStateOf(false)
-    private val _addContactAlias = mutableStateOf("")
-    private val _addContactLink = mutableStateOf("")
-
     val contactList: List<ContactItem> = _filteredContactList
     val filterBy: State<String> = _filterBy
     val selectedContact: State<ContactItem?> = _selectedContact
 
-    val addContactDialogVisible: State<Boolean> = _addContactDialogVisible
-    val addContactAlias: State<String> = _addContactAlias
-    val addContactLink: State<String> = _addContactLink
-    var addContactOwnLink = ""
-
     internal fun loadContacts() {
         _contactList.apply {
             clear()
@@ -79,8 +64,6 @@ constructor(
             })
         }
         updateFilteredList()
-        //todo: do in event instead?
-        addContactOwnLink = contactManager.handshakeLink
     }
 
     fun selectContact(index: Int) {
@@ -105,69 +88,6 @@ constructor(
         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
-    }
-
     override fun eventOccurred(e: Event?) {
         when (e) {
             is ContactAddedEvent -> {
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/AddContactDialog.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt
similarity index 80%
rename from src/main/kotlin/org/briarproject/briar/desktop/contact/AddContactDialog.kt
rename to src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt
index 7a623d2e8d0e25875b92d2d7e767dc8a38845832..f6da6679382d99a90f9c6bcd54f27742a5734f6f 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/AddContactDialog.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt
@@ -1,4 +1,4 @@
-package org.briarproject.briar.desktop.contact
+package org.briarproject.briar.desktop.contact.add.remote
 
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
@@ -15,6 +15,7 @@ import androidx.compose.material.Text
 import androidx.compose.material.TextButton
 import androidx.compose.material.TextField
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
@@ -22,9 +23,13 @@ import androidx.compose.ui.unit.sp
 
 @OptIn(ExperimentalMaterialApi::class)
 @Composable
-fun AddContactDialog(viewModel: ContactsViewModel) {
+fun AddContactDialog(viewModel: AddContactViewModel, onClose: () -> Unit) {
+    LaunchedEffect("fetchHandshake") {
+        //todo: should instead be done automatically as soon as DB is loaded -> in view model
+        viewModel.fetchHandshakeLink()
+    }
     AlertDialog(
-        onDismissRequest = viewModel::closeAddContactDialog,
+        onDismissRequest = onClose,
         text = {
             Column(modifier = Modifier.fillMaxWidth()) {
                 Row(Modifier.fillMaxWidth().padding(vertical = 16.dp)) {
@@ -40,8 +45,8 @@ fun AddContactDialog(viewModel: ContactsViewModel) {
                         Modifier.width(128.dp).align(Alignment.CenterVertically),
                     )
                     TextField(
-                        viewModel.addContactLink.value,
-                        viewModel::setAddContactLink,
+                        viewModel.remoteHandshakeLink.value,
+                        viewModel::setRemoteHandshakeLink,
                         modifier = Modifier.fillMaxWidth()
                     )
                 }
@@ -51,7 +56,7 @@ fun AddContactDialog(viewModel: ContactsViewModel) {
                         Modifier.width(128.dp).align(Alignment.CenterVertically),
                     )
                     TextField(
-                        viewModel.addContactAlias.value,
+                        viewModel.alias.value,
                         viewModel::setAddContactAlias,
                         modifier = Modifier.fillMaxWidth()
                     )
@@ -62,7 +67,7 @@ fun AddContactDialog(viewModel: ContactsViewModel) {
                         modifier = Modifier.width(128.dp).align(Alignment.CenterVertically),
                     )
                     TextField(
-                        viewModel.addContactOwnLink,
+                        viewModel.handshakeLink.value,
                         onValueChange = {},
                         modifier = Modifier.fillMaxWidth()
                     )
@@ -70,13 +75,13 @@ fun AddContactDialog(viewModel: ContactsViewModel) {
             }
         },
         confirmButton = {
-            Button(onClick = viewModel::onSubmitAddContactDialog) {
+            Button(onClick = { viewModel.onSubmitAddContactDialog(); onClose() }) {
                 Text("Add")
             }
         },
         dismissButton = {
             TextButton(
-                onClick = viewModel::closeAddContactDialog
+                onClick = onClose
             ) {
                 Text("Cancel", color = MaterialTheme.colors.onSurface)
             }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactViewModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a0b5d3ca04fac1cbf302f00fe38ad77f1deeecb7
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactViewModel.kt
@@ -0,0 +1,99 @@
+package org.briarproject.briar.desktop.contact.add.remote
+
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import org.briarproject.bramble.api.FormatException
+import org.briarproject.bramble.api.contact.ContactManager
+import org.briarproject.bramble.api.contact.HandshakeLinkConstants
+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
+
+class AddContactViewModel
+@Inject
+constructor(
+    private val contactManager: ContactManager,
+) {
+
+    companion object {
+        private val LOG = Logger.getLogger(AddContactViewModel::class.java.name)
+    }
+
+    private val _alias = mutableStateOf("")
+    private val _remoteHandshakeLink = mutableStateOf("")
+    private val _handshakeLink = mutableStateOf("")
+
+    val alias: State<String> = _alias
+    val remoteHandshakeLink: State<String> = _remoteHandshakeLink
+    val handshakeLink: State<String> = _handshakeLink
+
+    fun setAddContactAlias(alias: String) {
+        _alias.value = alias
+    }
+
+    fun setRemoteHandshakeLink(link: String) {
+        _remoteHandshakeLink.value = link
+    }
+
+    fun fetchHandshakeLink() {
+        _handshakeLink.value = contactManager.handshakeLink
+    }
+
+    fun onSubmitAddContactDialog() {
+        val link = _remoteHandshakeLink.value
+        val alias = _alias.value
+        addPendingContact(link, alias)
+    }
+
+    private fun addPendingContact(link: String, alias: String) {
+        if (_handshakeLink.value == link) {
+            println("Please enter contact's link, not your own")
+            return
+        }
+        if (remoteHandshakeLinkIsInvalid(link)) {
+            println("Remote handshake link is invalid")
+            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 remoteHandshakeLinkIsInvalid(link: String): Boolean {
+        return !HandshakeLinkConstants.LINK_REGEX.matcher(link).find()
+    }
+
+    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 48370f4fbf2ac77904e0fc9d7c646dbc8fb94a55..80d1585025f1c81bd011338dd175a5067428aeb7 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageView.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageView.kt
@@ -11,24 +11,26 @@ import androidx.compose.ui.Modifier
 import org.briarproject.briar.desktop.contact.ContactInfoDrawerState.MakeIntro
 import org.briarproject.briar.desktop.contact.ContactList
 import org.briarproject.briar.desktop.contact.ContactsViewModel
+import org.briarproject.briar.desktop.contact.add.remote.AddContactViewModel
 import org.briarproject.briar.desktop.ui.UiPlaceholder
 import org.briarproject.briar.desktop.ui.VerticalDivider
 
 @Composable
 fun PrivateMessageView(
-    contacts: ContactsViewModel,
+    contactsViewModel: ContactsViewModel,
+    addContactViewModel: AddContactViewModel,
 ) {
     val (dropdownExpanded, setExpanded) = remember { mutableStateOf(false) }
     val (infoDrawer, setInfoDrawer) = remember { mutableStateOf(false) }
     val (contactDrawerState, setDrawerState) = remember { mutableStateOf(MakeIntro) }
     Row(modifier = Modifier.fillMaxWidth()) {
-        ContactList(contacts)
+        ContactList(contactsViewModel, addContactViewModel)
         VerticalDivider()
         Column(modifier = Modifier.weight(1f).fillMaxHeight()) {
-            contacts.selectedContact.value?.also { selectedContact ->
+            contactsViewModel.selectedContact.value?.also { selectedContact ->
                 Conversation(
                     selectedContact.contact,
-                    contacts.contactList.map { c -> c.contact },
+                    contactsViewModel.contactList.map { c -> c.contact },
                     dropdownExpanded,
                     setExpanded,
                     infoDrawer,
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt b/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt
index 5fa8ea0d60bf2e40353233dd4e6b62f0a7017026..d20fffa6f2c69235aeb8e1e40f0960743a2360ef 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt
@@ -16,6 +16,7 @@ import org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RU
 import org.briarproject.briar.api.conversation.ConversationManager
 import org.briarproject.briar.api.messaging.MessagingManager
 import org.briarproject.briar.desktop.contact.ContactsViewModel
+import org.briarproject.briar.desktop.contact.add.remote.AddContactViewModel
 import org.briarproject.briar.desktop.login.Login
 import org.briarproject.briar.desktop.login.LoginViewModel
 import org.briarproject.briar.desktop.login.Registration
@@ -53,6 +54,7 @@ constructor(
     private val registrationViewModel: RegistrationViewModel,
     private val loginViewModel: LoginViewModel,
     private val contactsViewModel: ContactsViewModel,
+    private val addContactViewModel: AddContactViewModel,
     private val accountManager: AccountManager,
     private val contactManager: ContactManager,
     private val conversationManager: ConversationManager,
@@ -115,7 +117,7 @@ constructor(
                                 MM provides messagingManager,
                                 IM provides identityManager,
                             ) {
-                                MainScreen(contactsViewModel, isDark, setDark)
+                                MainScreen(contactsViewModel, addContactViewModel, isDark, setDark)
                             }
                     }
                 }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/ui/MainScreen.kt b/src/main/kotlin/org/briarproject/briar/desktop/ui/MainScreen.kt
index b5da9523549c366aad454d609d2d3708699dae24..74f57ea8046d4ddaae8bbc0fab0fca38aa046b5d 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/ui/MainScreen.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/ui/MainScreen.kt
@@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import org.briarproject.briar.desktop.contact.ContactsViewModel
+import org.briarproject.briar.desktop.contact.add.remote.AddContactViewModel
 import org.briarproject.briar.desktop.conversation.PrivateMessageView
 import org.briarproject.briar.desktop.navigation.BriarSidebar
 import org.briarproject.briar.desktop.settings.PlaceHolderSettingsView
@@ -17,6 +18,7 @@ import org.briarproject.briar.desktop.settings.PlaceHolderSettingsView
 @Composable
 fun MainScreen(
     contactsViewModel: ContactsViewModel,
+    addContactViewModel: AddContactViewModel,
     isDark: Boolean,
     setDark: (Boolean) -> Unit
 ) {
@@ -27,7 +29,7 @@ fun MainScreen(
         BriarSidebar(uiMode, setUiMode)
         VerticalDivider()
         when (uiMode) {
-            UiMode.CONTACTS -> PrivateMessageView(contactsViewModel)
+            UiMode.CONTACTS -> PrivateMessageView(contactsViewModel, addContactViewModel)
             UiMode.SETTINGS -> PlaceHolderSettingsView(isDark, setDark)
             else -> UiPlaceholder()
         }