diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDropDown.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDropDown.kt
index 9740a14c63c3154b05b92580b51069e55d12b489..70c73a89f615bad46840c163eb99d03bb6b696cf 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDropDown.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDropDown.kt
@@ -22,7 +22,7 @@ import androidx.compose.ui.unit.sp
 fun ContactDropDown(
     expanded: Boolean,
     isExpanded: (Boolean) -> Unit,
-    setInfoDrawer: (Boolean) -> Unit
+    onMakeIntroduction: () -> Unit,
 ) {
     var connectionMode by remember { mutableStateOf(false) }
     var contactMode by remember { mutableStateOf(false) }
@@ -30,7 +30,7 @@ fun ContactDropDown(
         expanded = expanded,
         onDismissRequest = { isExpanded(false) },
     ) {
-        DropdownMenuItem(onClick = { setInfoDrawer(true); isExpanded(false) }) {
+        DropdownMenuItem(onClick = { isExpanded(false); onMakeIntroduction() }) {
             Text("Make Introduction", fontSize = 14.sp)
         }
         DropdownMenuItem(onClick = {}) {
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactInfoDrawer.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactInfoDrawer.kt
index 99f247b2d9995ae95e7568f56d8a6d3062b2e0fb..f43736a975bd38b7427f2e45fc0ba9782d447131 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactInfoDrawer.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactInfoDrawer.kt
@@ -1,8 +1,9 @@
 package org.briarproject.briar.desktop.contact
 
 import androidx.compose.runtime.Composable
-import org.briarproject.bramble.api.contact.Contact
 import org.briarproject.briar.desktop.contact.ContactInfoDrawerState.MakeIntro
+import org.briarproject.briar.desktop.introduction.ContactDrawerMakeIntro
+import org.briarproject.briar.desktop.introduction.IntroductionViewModel
 
 // Right drawer state
 enum class ContactInfoDrawerState {
@@ -13,12 +14,11 @@ enum class ContactInfoDrawerState {
 
 @Composable
 fun ContactInfoDrawer(
-    contact: Contact,
-    contacts: List<Contact>,
+    introductionViewModel: IntroductionViewModel,
     setInfoDrawer: (Boolean) -> Unit,
     drawerState: ContactInfoDrawerState
 ) {
     when (drawerState) {
-        MakeIntro -> ContactDrawerMakeIntro(contact, contacts, setInfoDrawer)
+        MakeIntro -> ContactDrawerMakeIntro(introductionViewModel, setInfoDrawer)
     }
 }
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 10c12cbdce721addc9455b316ecf766bb6d54b83..a20bb5bf3fc5edf6d09c96eb410a7cb76810730a 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItem.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItem.kt
@@ -3,19 +3,24 @@ 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,
-    val isEmpty: Boolean = groupCount.msgCount == 0,
-    val unread: Int = groupCount.unreadCount,
-    val timestamp: Long = groupCount.latestMsgTime
+    val isEmpty: Boolean,
+    val unread: Int,
+    val timestamp: Long
 ) {
+
+    constructor(contact: Contact, isConnected: Boolean, groupCount: MessageTracker.GroupCount)
+            : this(
+        contact, isConnected,
+        isEmpty = groupCount.msgCount == 0,
+        unread = groupCount.unreadCount,
+        timestamp = groupCount.latestMsgTime
+    )
+
     fun updateFromMessageHeader(h: ConversationMessageHeader): ContactItem {
         return copy(
             isEmpty = false,
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 7fde6fe404e140b6f41e4e285f8fb34d0dfcff9a..4e769de56af0ec0447e45ebf101fed93f3042e5d 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt
@@ -24,7 +24,7 @@ import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE
 
 @Composable
 fun ContactList(
-    viewModel: ContactsViewModel,
+    viewModel: ContactListViewModel,
     addContactViewModel: AddContactViewModel,
 ) {
     var isContactDialogVisible by remember { mutableStateOf(false) }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f9a3438af8de91d3e81f3a2c257e6413f43ade7a
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt
@@ -0,0 +1,94 @@
+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.connection.ConnectionRegistry
+import org.briarproject.bramble.api.contact.ContactId
+import org.briarproject.bramble.api.contact.ContactManager
+import org.briarproject.bramble.api.contact.event.ContactAliasChangedEvent
+import org.briarproject.bramble.api.event.Event
+import org.briarproject.bramble.api.event.EventBus
+import org.briarproject.briar.api.conversation.ConversationManager
+import org.briarproject.briar.api.conversation.event.ConversationMessageReceivedEvent
+import java.util.logging.Logger
+import javax.inject.Inject
+
+class ContactListViewModel
+@Inject
+constructor(
+    contactManager: ContactManager,
+    conversationManager: ConversationManager,
+    connectionRegistry: ConnectionRegistry,
+    eventBus: EventBus,
+) : ContactsViewModel(contactManager, conversationManager, connectionRegistry) {
+
+    companion object {
+        private val LOG = Logger.getLogger(ContactListViewModel::class.java.name)
+    }
+
+    init {
+        //todo: where/when to remove listener again?
+        eventBus.addListener(this)
+    }
+
+    private val _filteredContactList = mutableStateListOf<ContactItem>()
+    private val _filterBy = mutableStateOf("")
+    private var _selectedContactIndex = -1;
+    private val _selectedContact = mutableStateOf<ContactItem?>(null)
+
+    override val contactList: List<ContactItem> = _filteredContactList
+    val filterBy: State<String> = _filterBy
+    val selectedContact: State<ContactItem?> = _selectedContact
+
+    override fun loadContacts() {
+        super.loadContacts()
+        updateFilteredList()
+    }
+
+    fun selectContact(index: Int) {
+        _selectedContactIndex = index
+        _selectedContact.value = _filteredContactList[index]
+    }
+
+    fun isSelected(index: Int) = _selectedContactIndex == index
+
+    private fun updateFilteredList() {
+        _filteredContactList.apply {
+            clear()
+            addAll(_contactList.filter {
+                // todo: also filter on alias?
+                it.contact.author.name.lowercase().contains(_filterBy.value)
+            })
+        }
+    }
+
+    fun setFilterBy(filter: String) {
+        _filterBy.value = filter
+        updateFilteredList()
+    }
+
+    override fun eventOccurred(e: Event?) {
+        super.eventOccurred(e)
+        when (e) {
+            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) }
+            }
+        }
+    }
+
+    override fun updateItem(contactId: ContactId, update: (ContactItem) -> ContactItem) {
+        super.updateItem(contactId, update)
+        updateFilteredList()
+    }
+
+    override fun removeItem(contactId: ContactId) {
+        super.removeItem(contactId)
+        updateFilteredList()
+    }
+}
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 32da44de5078d3351a54c97cea358fab413be821..61a56229033a80392daf47ad76827bef6c798c9f 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt
@@ -1,91 +1,47 @@
 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.connection.ConnectionRegistry
+import org.briarproject.bramble.api.contact.Contact
 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.event.Event
-import org.briarproject.bramble.api.event.EventBus
 import org.briarproject.bramble.api.event.EventListener
 import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent
 import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent
 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.util.logging.Logger
-import javax.inject.Inject
 
-class ContactsViewModel
-@Inject
-constructor(
-    private val contactManager: ContactManager,
-    private val authorManager: AuthorManager,
+abstract class ContactsViewModel(
+    protected val contactManager: ContactManager,
     private val conversationManager: ConversationManager,
-    private val connectionRegistry: ConnectionRegistry,
-    private val eventBus: EventBus,
+    private val connectionRegistry: ConnectionRegistry
 ) : EventListener {
 
     companion object {
         private val LOG = Logger.getLogger(ContactsViewModel::class.java.name)
     }
 
-    init {
-        //todo: where/when to remove listener again?
-        eventBus.addListener(this)
-    }
+    protected val _contactList = mutableListOf<ContactItem>()
 
-    private val _contactList = mutableListOf<ContactItem>()
-    private val _filteredContactList = mutableStateListOf<ContactItem>()
-    private val _filterBy = mutableStateOf("")
-    private var _selectedContactIndex = -1;
-    private val _selectedContact = mutableStateOf<ContactItem?>(null)
+    abstract val contactList: List<ContactItem>
 
-    val contactList: List<ContactItem> = _filteredContactList
-    val filterBy: State<String> = _filterBy
-    val selectedContact: State<ContactItem?> = _selectedContact
+    protected open fun filterContact(contact: Contact) = true
 
-    internal fun loadContacts() {
+    open fun loadContacts() {
         _contactList.apply {
             clear()
-            addAll(contactManager.contacts.map { contact ->
+            addAll(contactManager.contacts.filter(::filterContact).map { contact ->
                 ContactItem(
                     contact,
-                    authorManager.getAuthorInfo(contact),
+                    connectionRegistry.isConnected(contact.id),
                     conversationManager.getGroupCount(contact.id),
-                    connectionRegistry.isConnected(contact.id)
                 )
             })
         }
-        updateFilteredList()
-    }
-
-    fun selectContact(index: Int) {
-        _selectedContactIndex = index
-        _selectedContact.value = _filteredContactList[index]
-    }
-
-    fun isSelected(index: Int) = _selectedContactIndex == index
-
-    private fun updateFilteredList() {
-        _filteredContactList.apply {
-            clear()
-            addAll(_contactList.filter {
-                // todo: also filter on alias?
-                it.contact.author.name.lowercase().contains(_filterBy.value)
-            })
-        }
-    }
-
-    fun setFilterBy(filter: String) {
-        _filterBy.value = filter
-        updateFilteredList()
     }
 
     override fun eventOccurred(e: Event?) {
@@ -106,24 +62,14 @@ constructor(
                 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) {
+    protected open fun updateItem(contactId: ContactId, update: (ContactItem) -> ContactItem) {
         _contactList.replaceFirst({ it.contact.id == contactId }, update)
-        updateFilteredList()
     }
 
-    private fun removeItem(contactId: ContactId) {
+    protected open fun removeItem(contactId: ContactId) {
         _contactList.removeFirst { it.contact.id == contactId }
-        updateFilteredList()
     }
 }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/Conversation.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/Conversation.kt
index 71f0cd34e0d8295041b8d35a331424c3cd62c2ed..23c61666c1115c531806d12bec299ece7df21be9 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/Conversation.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/Conversation.kt
@@ -18,6 +18,7 @@ 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.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -25,6 +26,7 @@ import androidx.compose.ui.unit.dp
 import org.briarproject.bramble.api.contact.Contact
 import org.briarproject.briar.desktop.contact.ContactInfoDrawer
 import org.briarproject.briar.desktop.contact.ContactInfoDrawerState
+import org.briarproject.briar.desktop.introduction.IntroductionViewModel
 import org.briarproject.briar.desktop.navigation.SIDEBAR_WIDTH
 import org.briarproject.briar.desktop.theme.surfaceVariant
 import org.briarproject.briar.desktop.ui.Constants.CONTACTLIST_WIDTH
@@ -32,17 +34,25 @@ import org.briarproject.briar.desktop.ui.Constants.CONTACTLIST_WIDTH
 @Composable
 fun Conversation(
     contact: Contact,
-    contacts: List<Contact>,
-    expanded: Boolean,
-    setExpanded: (Boolean) -> Unit,
-    infoDrawer: Boolean,
-    setInfoDrawer: (Boolean) -> Unit,
-    drawerState: ContactInfoDrawerState
+    introductionViewModel: IntroductionViewModel,
 ) {
+    val (infoDrawer, setInfoDrawer) = remember { mutableStateOf(false) }
+    val (contactDrawerState, setDrawerState) = remember { mutableStateOf(ContactInfoDrawerState.MakeIntro) }
     BoxWithConstraints(Modifier.fillMaxSize()) {
         val animatedInfoDrawerOffsetX by animateDpAsState(if (infoDrawer) (-275).dp else 0.dp)
         Scaffold(
-            topBar = { ConversationHeader(contact, expanded, setExpanded, setInfoDrawer) },
+            topBar = {
+                ConversationHeader(
+                    contact,
+                    onMakeIntroduction = {
+                        introductionViewModel.apply {
+                            setFirstContact(contact)
+                            loadContacts()
+                        }
+                        setInfoDrawer(true)
+                    }
+                )
+            },
             content = { padding ->
                 Box(modifier = Modifier.padding(padding)) {
                     val chat = ChatState(contact.id)
@@ -71,7 +81,7 @@ fun Conversation(
                         RoundedCornerShape(topStart = 10.dp, bottomStart = 10.dp)
                     )
             ) {
-                ContactInfoDrawer(contact, contacts, setInfoDrawer, drawerState)
+                ContactInfoDrawer(introductionViewModel, setInfoDrawer, contactDrawerState)
             }
         }
     }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationHeader.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationHeader.kt
index c8c45f0e05b09738cef58bafb7f40507901c3c9a..a36dca368671dfc5302184dab43c84e45e50a6a5 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationHeader.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationHeader.kt
@@ -14,6 +14,8 @@ import androidx.compose.material.Text
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.MoreVert
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.drawscope.withTransform
@@ -29,10 +31,9 @@ import org.briarproject.briar.desktop.ui.HorizontalDivider
 @Composable
 fun ConversationHeader(
     contact: Contact,
-    expanded: Boolean,
-    isExpanded: (Boolean) -> Unit,
-    setInfoDrawer: (Boolean) -> Unit
+    onMakeIntroduction: () -> Unit,
 ) {
+    val (isExpanded, setExpanded) = remember { mutableStateOf(false) }
     // TODO hook up online indicator logic
     val onlineColor = MaterialTheme.colors.secondary
     val outlineColor = MaterialTheme.colors.outline
@@ -57,11 +58,11 @@ fun ConversationHeader(
             )
         }
         IconButton(
-            onClick = { isExpanded(!expanded) },
+            onClick = { setExpanded(!isExpanded) },
             modifier = Modifier.align(Alignment.CenterEnd).padding(end = 16.dp)
         ) {
             Icon(Icons.Filled.MoreVert, "contact info", modifier = Modifier.size(24.dp))
-            ContactDropDown(expanded, isExpanded, setInfoDrawer)
+            ContactDropDown(isExpanded, setExpanded, onMakeIntroduction)
         }
         HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter))
     }
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 80d1585025f1c81bd011338dd175a5067428aeb7..ae06e0135a6b6a8c462bd8a72497add5f8bfcefa 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageView.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageView.kt
@@ -5,37 +5,28 @@ import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxWidth
 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.ContactInfoDrawerState.MakeIntro
 import org.briarproject.briar.desktop.contact.ContactList
-import org.briarproject.briar.desktop.contact.ContactsViewModel
+import org.briarproject.briar.desktop.contact.ContactListViewModel
 import org.briarproject.briar.desktop.contact.add.remote.AddContactViewModel
+import org.briarproject.briar.desktop.introduction.IntroductionViewModel
 import org.briarproject.briar.desktop.ui.UiPlaceholder
 import org.briarproject.briar.desktop.ui.VerticalDivider
 
 @Composable
 fun PrivateMessageView(
-    contactsViewModel: ContactsViewModel,
+    contactListViewModel: ContactListViewModel,
     addContactViewModel: AddContactViewModel,
+    introductionViewModel: IntroductionViewModel,
 ) {
-    val (dropdownExpanded, setExpanded) = remember { mutableStateOf(false) }
-    val (infoDrawer, setInfoDrawer) = remember { mutableStateOf(false) }
-    val (contactDrawerState, setDrawerState) = remember { mutableStateOf(MakeIntro) }
     Row(modifier = Modifier.fillMaxWidth()) {
-        ContactList(contactsViewModel, addContactViewModel)
+        ContactList(contactListViewModel, addContactViewModel)
         VerticalDivider()
         Column(modifier = Modifier.weight(1f).fillMaxHeight()) {
-            contactsViewModel.selectedContact.value?.also { selectedContact ->
+            contactListViewModel.selectedContact.value?.also { selectedContact ->
                 Conversation(
                     selectedContact.contact,
-                    contactsViewModel.contactList.map { c -> c.contact },
-                    dropdownExpanded,
-                    setExpanded,
-                    infoDrawer,
-                    setInfoDrawer,
-                    contactDrawerState
+                    introductionViewModel
                 )
             } ?: UiPlaceholder()
         }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDrawerMakeIntro.kt b/src/main/kotlin/org/briarproject/briar/desktop/introduction/ContactDrawerMakeIntro.kt
similarity index 55%
rename from src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDrawerMakeIntro.kt
rename to src/main/kotlin/org/briarproject/briar/desktop/introduction/ContactDrawerMakeIntro.kt
index 76e4c31547f7f835257621f1920b2ddd015d7a82..b140e1fa65051edec6a51d59b867b5ee15013b86 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDrawerMakeIntro.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/introduction/ContactDrawerMakeIntro.kt
@@ -1,4 +1,4 @@
-package org.briarproject.briar.desktop.contact
+package org.briarproject.briar.desktop.introduction
 
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
@@ -7,8 +7,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
 import androidx.compose.material.Icon
 import androidx.compose.material.IconButton
 import androidx.compose.material.Surface
@@ -20,45 +20,45 @@ import androidx.compose.material.icons.filled.ArrowBack
 import androidx.compose.material.icons.filled.Close
 import androidx.compose.material.icons.filled.SwapHoriz
 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.graphics.Color
-import androidx.compose.ui.text.input.TextFieldValue
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
-import org.briarproject.bramble.api.contact.Contact
+import org.briarproject.briar.desktop.contact.ContactCard
+import org.briarproject.briar.desktop.contact.ProfileCircle
 import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE
 import org.briarproject.briar.desktop.ui.HorizontalDivider
 
 @Composable
-fun ContactDrawerMakeIntro(contact: Contact, contacts: List<Contact>, setInfoDrawer: (Boolean) -> Unit) {
-    var introNextPg by remember { mutableStateOf(false) }
-    val (introContact, onCancelSel) = remember { mutableStateOf(contact) }
-    if (!introNextPg) {
+fun ContactDrawerMakeIntro(
+    viewModel: IntroductionViewModel,
+    setInfoDrawer: (Boolean) -> Unit
+) {
+    if (!viewModel.secondScreen.value) {
         Surface {
-            Row(Modifier.fillMaxWidth().height(HEADER_SIZE)) {
-                IconButton(
-                    onClick = { setInfoDrawer(false) },
-                    Modifier.padding(horizontal = 11.dp).size(32.dp).align(Alignment.CenterVertically)
-                ) {
-                    Icon(Icons.Filled.Close, "close make intro screen")
+            Column {
+                Row(Modifier.fillMaxWidth().height(HEADER_SIZE)) {
+                    IconButton(
+                        onClick = { setInfoDrawer(false) },
+                        Modifier.padding(horizontal = 11.dp).size(32.dp).align(Alignment.CenterVertically)
+                    ) {
+                        Icon(Icons.Filled.Close, "close make intro screen")
+                    }
+                    Text(
+                        text = "Introduce " + viewModel.firstContact.value!!.author.name + " to:",
+                        fontSize = 16.sp,
+                        modifier = Modifier.align(Alignment.CenterVertically)
+                    )
                 }
-                Text(
-                    text = "Introduce " + contact.author.name + " to:",
-                    fontSize = 16.sp,
-                    modifier = Modifier.align(Alignment.CenterVertically)
-                )
-            }
-            HorizontalDivider()
-            Column(Modifier.verticalScroll(rememberScrollState())) {
-                for (c in contacts) {
-                    if (c.id != contact.id) {
-                        // todo: refactor to use contactItem in IntroductionViewModel
-                        //ContactCard(c, { onCancelSel(c); introNextPg = true }, false, false)
+                HorizontalDivider()
+                LazyColumn {
+                    items(viewModel.contactList) { contactItem ->
+                        ContactCard(
+                            contactItem,
+                            { viewModel.setSecondContact(contactItem.contact) },
+                            false
+                        )
                     }
                 }
             }
@@ -67,7 +67,7 @@ fun ContactDrawerMakeIntro(contact: Contact, contacts: List<Contact>, setInfoDra
         Column {
             Row(Modifier.fillMaxWidth().height(HEADER_SIZE)) {
                 IconButton(
-                    onClick = { introNextPg = false },
+                    onClick = viewModel::backToFirstScreen,
                     Modifier.padding(horizontal = 11.dp).size(32.dp).align(Alignment.CenterVertically)
                 ) {
                     Icon(Icons.Filled.ArrowBack, "go back to make intro contact screen", tint = Color.White)
@@ -80,26 +80,25 @@ fun ContactDrawerMakeIntro(contact: Contact, contacts: List<Contact>, setInfoDra
             }
             Row(Modifier.fillMaxWidth().padding(12.dp), horizontalArrangement = Arrangement.SpaceAround) {
                 Column(Modifier.align(Alignment.CenterVertically)) {
-                    ProfileCircle(36.dp, contact.author.id.bytes)
-                    Text(contact.author.name, Modifier.padding(top = 4.dp), Color.White, 16.sp)
+                    ProfileCircle(36.dp, viewModel.firstContact.value!!.author.id.bytes)
+                    Text(viewModel.firstContact.value!!.author.name, Modifier.padding(top = 4.dp), Color.White, 16.sp)
                 }
                 Icon(Icons.Filled.SwapHoriz, "swap", modifier = Modifier.size(48.dp))
                 Column(Modifier.align(Alignment.CenterVertically)) {
-                    ProfileCircle(36.dp, introContact.author.id.bytes)
-                    Text(introContact.author.name, Modifier.padding(top = 4.dp), Color.White, 16.sp)
+                    ProfileCircle(36.dp, viewModel.secondContact.value!!.author.id.bytes)
+                    Text(viewModel.secondContact.value!!.author.name, Modifier.padding(top = 4.dp), Color.White, 16.sp)
                 }
             }
-            var introText by remember { mutableStateOf(TextFieldValue("")) }
             Row(Modifier.padding(8.dp)) {
                 TextField(
-                    introText,
-                    { introText = it },
+                    viewModel.introductionMessage.value,
+                    viewModel::setIntroductionMessage,
                     placeholder = { Text(text = "Add a message (optional)") },
                 )
             }
             Row(Modifier.padding(8.dp)) {
                 TextButton(
-                    onClick = { setInfoDrawer(false); introNextPg = false; },
+                    onClick = { setInfoDrawer(false) },
                     Modifier.fillMaxWidth()
                 ) {
                     Text("MAKE INTRODUCTION")
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/introduction/IntroductionViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/introduction/IntroductionViewModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0d64fda2b80053a45b20fea56dafc9084bbd82dd
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/introduction/IntroductionViewModel.kt
@@ -0,0 +1,66 @@
+package org.briarproject.briar.desktop.introduction
+
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import org.briarproject.bramble.api.connection.ConnectionRegistry
+import org.briarproject.bramble.api.contact.Contact
+import org.briarproject.bramble.api.contact.ContactManager
+import org.briarproject.bramble.api.event.EventBus
+import org.briarproject.briar.api.conversation.ConversationManager
+import org.briarproject.briar.desktop.contact.ContactItem
+import org.briarproject.briar.desktop.contact.ContactsViewModel
+import java.util.logging.Logger
+import javax.inject.Inject
+
+class IntroductionViewModel
+@Inject
+constructor(
+    contactManager: ContactManager,
+    conversationManager: ConversationManager,
+    connectionRegistry: ConnectionRegistry,
+    eventBus: EventBus,
+) : ContactsViewModel(contactManager, conversationManager, connectionRegistry) {
+
+    companion object {
+        private val LOG = Logger.getLogger(IntroductionViewModel::class.java.name)
+    }
+
+    init {
+        //todo: where/when to remove listener again?
+        eventBus.addListener(this)
+    }
+
+    private val _firstContact = mutableStateOf<Contact?>(null)
+    private val _secondContact = mutableStateOf<Contact?>(null)
+    private val _secondScreen = mutableStateOf(false)
+    private val _introductionMessage = mutableStateOf("")
+
+    override val contactList: List<ContactItem> = _contactList
+    val firstContact: State<Contact?> = _firstContact
+    val secondContact: State<Contact?> = _secondContact
+    val secondScreen: State<Boolean> = _secondScreen
+    val introductionMessage: State<String> = _introductionMessage
+
+    fun setFirstContact(contact: Contact) {
+        _firstContact.value = contact
+        loadContacts()
+        backToFirstScreen()
+    }
+
+    fun setSecondContact(contact: Contact) {
+        _secondContact.value = contact
+        _secondScreen.value = true
+    }
+
+    fun backToFirstScreen() {
+        _secondScreen.value = false
+    }
+
+    fun setIntroductionMessage(msg: String) {
+        _introductionMessage.value = msg
+    }
+
+    override fun filterContact(contact: Contact): Boolean {
+        return _firstContact.value!!.id != contact.id
+    }
+}
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 d20fffa6f2c69235aeb8e1e40f0960743a2360ef..afbda2623fdeeb8c14b7392255ea4193880b0acb 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt
@@ -15,8 +15,9 @@ import org.briarproject.bramble.api.lifecycle.LifecycleManager
 import org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING
 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.ContactListViewModel
 import org.briarproject.briar.desktop.contact.add.remote.AddContactViewModel
+import org.briarproject.briar.desktop.introduction.IntroductionViewModel
 import org.briarproject.briar.desktop.login.Login
 import org.briarproject.briar.desktop.login.LoginViewModel
 import org.briarproject.briar.desktop.login.Registration
@@ -53,8 +54,9 @@ internal class BriarUiImpl
 constructor(
     private val registrationViewModel: RegistrationViewModel,
     private val loginViewModel: LoginViewModel,
-    private val contactsViewModel: ContactsViewModel,
+    private val contactListViewModel: ContactListViewModel,
     private val addContactViewModel: AddContactViewModel,
+    private val introductionViewModel: IntroductionViewModel,
     private val accountManager: AccountManager,
     private val contactManager: ContactManager,
     private val conversationManager: ConversationManager,
@@ -84,7 +86,7 @@ constructor(
                     if (accountManager.hasDatabaseKey()) {
                         // this should only happen during testing when we launch the main UI directly
                         // without a need to enter the password.
-                        contactsViewModel.loadContacts()
+                        contactListViewModel.loadContacts()
                         Screen.MAIN
                     } else if (accountManager.accountExists()) {
                         Screen.LOGIN
@@ -102,12 +104,12 @@ constructor(
                     when (screenState) {
                         Screen.REGISTRATION ->
                             Registration(registrationViewModel) {
-                                contactsViewModel.loadContacts()
+                                contactListViewModel.loadContacts()
                                 screenState = Screen.MAIN
                             }
                         Screen.LOGIN ->
                             Login(loginViewModel) {
-                                contactsViewModel.loadContacts()
+                                contactListViewModel.loadContacts()
                                 screenState = Screen.MAIN
                             }
                         else ->
@@ -117,7 +119,13 @@ constructor(
                                 MM provides messagingManager,
                                 IM provides identityManager,
                             ) {
-                                MainScreen(contactsViewModel, addContactViewModel, isDark, setDark)
+                                MainScreen(
+                                    contactListViewModel,
+                                    addContactViewModel,
+                                    introductionViewModel,
+                                    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 74f57ea8046d4ddaae8bbc0fab0fca38aa046b5d..96622e0c932e6318b7dec7a94aa5756cbff42539 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/ui/MainScreen.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/ui/MainScreen.kt
@@ -4,9 +4,10 @@ import androidx.compose.foundation.layout.Row
 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.ContactListViewModel
 import org.briarproject.briar.desktop.contact.add.remote.AddContactViewModel
 import org.briarproject.briar.desktop.conversation.PrivateMessageView
+import org.briarproject.briar.desktop.introduction.IntroductionViewModel
 import org.briarproject.briar.desktop.navigation.BriarSidebar
 import org.briarproject.briar.desktop.settings.PlaceHolderSettingsView
 
@@ -17,8 +18,9 @@ import org.briarproject.briar.desktop.settings.PlaceHolderSettingsView
  */
 @Composable
 fun MainScreen(
-    contactsViewModel: ContactsViewModel,
+    contactListViewModel: ContactListViewModel,
     addContactViewModel: AddContactViewModel,
+    introductionViewModel: IntroductionViewModel,
     isDark: Boolean,
     setDark: (Boolean) -> Unit
 ) {
@@ -29,7 +31,7 @@ fun MainScreen(
         BriarSidebar(uiMode, setUiMode)
         VerticalDivider()
         when (uiMode) {
-            UiMode.CONTACTS -> PrivateMessageView(contactsViewModel, addContactViewModel)
+            UiMode.CONTACTS -> PrivateMessageView(contactListViewModel, addContactViewModel, introductionViewModel)
             UiMode.SETTINGS -> PlaceHolderSettingsView(isDark, setDark)
             else -> UiPlaceholder()
         }