diff --git a/src/main/kotlin/org/briarproject/briar/desktop/Main.kt b/src/main/kotlin/org/briarproject/briar/desktop/Main.kt
index 1c401a2cfc3fbcdd11e814673272a4bd9b6ccacd..d2347fa819a7bf5babfd915804941839b6ee3aec 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/Main.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/Main.kt
@@ -8,6 +8,7 @@ import com.github.ajalt.clikt.parameters.options.flag
 import com.github.ajalt.clikt.parameters.options.option
 import org.briarproject.bramble.BrambleCoreEagerSingletons
 import org.briarproject.briar.BriarCoreEagerSingletons
+import org.briarproject.briar.desktop.utils.FileUtils
 import java.io.File.separator
 import java.io.IOException
 import java.lang.System.getProperty
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/AddContactDialog.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/AddContactDialog.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ec879bd3c4788bdd343a29c5e783fe4f1df549e0
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/AddContactDialog.kt
@@ -0,0 +1,142 @@
+package org.briarproject.briar.desktop.conversation
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.AlertDialog
+import androidx.compose.material.Button
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.MaterialTheme
+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.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
+    AlertDialog(
+        onDismissRequest = { setDialogVisibility(false) },
+        text = {
+            Column(modifier = Modifier.fillMaxWidth()) {
+                Row(Modifier.fillMaxWidth().padding(vertical = 16.dp)) {
+                    Text(
+                        text = "Add Contact at a Distance",
+                        fontSize = 24.sp,
+                        modifier = Modifier.align(Alignment.CenterVertically)
+                    )
+                }
+                Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
+                    Text(
+                        "Contact's Link",
+                        Modifier.width(128.dp).align(Alignment.CenterVertically),
+                    )
+                    TextField(contactLink, onValueChange = { contactLink = it }, 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())
+                }
+                Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
+                    Text(
+                        "Your Link",
+                        modifier = Modifier.width(128.dp).align(Alignment.CenterVertically),
+                    )
+                    TextField(
+                        ownLink,
+                        onValueChange = {},
+                        modifier = Modifier.fillMaxWidth()
+                    )
+                }
+            }
+        },
+        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)
+                },
+            ) {
+                Text("Add")
+            }
+        },
+        dismissButton = {
+            TextButton(
+                onClick = { setDialogVisibility(false) }
+            ) {
+                Text("Cancel", color = MaterialTheme.colors.onSurface)
+            }
+        },
+        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/ContactCard.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9af1d64f1fb90cd0ae981ce79fe003d6a6691f57
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt
@@ -0,0 +1,98 @@
+package org.briarproject.briar.desktop.conversation
+
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+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.shape.RoundedCornerShape
+import androidx.compose.material.Card
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+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.CVM
+import org.briarproject.briar.desktop.contact.ProfileCircle
+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.Constants.HEADER_SIZE
+import org.briarproject.briar.desktop.utils.TimeUtils.getFormattedTimestamp
+
+@Composable
+fun ContactCard(
+    contact: Contact,
+    selContact: Contact,
+    onSel: (Contact) -> Unit,
+    selected: Boolean,
+    drawNotifications: Boolean
+) {
+    val bgColor = if (selected) MaterialTheme.colors.selectedCard else MaterialTheme.colors.surfaceVariant
+    val outlineColor = MaterialTheme.colors.outline
+    val briarPrimary = MaterialTheme.colors.primary
+    val briarSecondary = MaterialTheme.colors.secondary
+    val briarSurfaceVar = MaterialTheme.colors.surfaceVariant
+
+    Card(
+        modifier = Modifier.fillMaxWidth().height(HEADER_SIZE).clickable(onClick = { onSel(contact) }),
+        shape = RoundedCornerShape(0.dp),
+        backgroundColor = bgColor,
+        contentColor = MaterialTheme.colors.onSurface
+    ) {
+        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)
+                // Draw notification badges
+                if (drawNotifications) {
+                    Canvas(
+                        modifier = Modifier.align(Alignment.CenterVertically),
+                        onDraw = {
+                            val size = 10.dp.toPx()
+                            withTransform({ translate(left = -6f, top = -12f) }) {
+                                drawCircle(color = outlineColor, radius = (size + 2.dp.toPx()) / 2f)
+                                drawCircle(color = briarPrimary, radius = size / 2f)
+                            }
+                        }
+                    )
+                }
+                Column(modifier = Modifier.align(Alignment.CenterVertically).padding(start = 12.dp)) {
+                    Text(
+                        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),
+                        fontSize = 10.sp,
+                        modifier = Modifier.align(Alignment.Start)
+                    )
+                }
+            }
+            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)
+                    }
+                }
+            )
+        }
+    }
+    HorizontalDivider()
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDrawerMakeIntro.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDrawerMakeIntro.kt
new file mode 100644
index 0000000000000000000000000000000000000000..fcb166e9151a4359da2dc79e3bb270d4af5f3bd8
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDrawerMakeIntro.kt
@@ -0,0 +1,109 @@
+package org.briarproject.briar.desktop.conversation
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+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.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.material.TextButton
+import androidx.compose.material.TextField
+import androidx.compose.material.icons.Icons
+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.ProfileCircle
+import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE
+
+@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) {
+        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")
+                }
+                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) {
+                        ContactCard(c, contact, { onCancelSel(c); introNextPg = true }, false, false)
+                    }
+                }
+            }
+        }
+    } else {
+        Column {
+            Row(Modifier.fillMaxWidth().height(HEADER_SIZE)) {
+                IconButton(
+                    onClick = { introNextPg = false },
+                    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)
+                }
+                Text(
+                    text = "Introduce Contacts",
+                    fontSize = 16.sp,
+                    modifier = Modifier.align(Alignment.CenterVertically)
+                )
+            }
+            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)
+                }
+                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)
+                }
+            }
+            var introText by remember { mutableStateOf(TextFieldValue("")) }
+            Row(Modifier.padding(8.dp)) {
+                TextField(
+                    introText,
+                    { introText = it },
+                    placeholder = { Text(text = "Add a message (optional)") },
+                )
+            }
+            Row(Modifier.padding(8.dp)) {
+                TextButton(
+                    onClick = { setInfoDrawer(false); introNextPg = false; },
+                    Modifier.fillMaxWidth()
+                ) {
+                    Text("MAKE INTRODUCTION")
+                }
+            }
+        }
+    }
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDropDown.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDropDown.kt
new file mode 100644
index 0000000000000000000000000000000000000000..caaf8fad7dee40cc1a51ebafd09d9aa474d4fe95
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactDropDown.kt
@@ -0,0 +1,87 @@
+package org.briarproject.briar.desktop.conversation
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material.DropdownMenu
+import androidx.compose.material.DropdownMenuItem
+import androidx.compose.material.Icon
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowRight
+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.sp
+
+@Composable
+fun ContactDropDown(
+    expanded: Boolean,
+    isExpanded: (Boolean) -> Unit,
+    setInfoDrawer: (Boolean) -> Unit
+) {
+    var connectionMode by remember { mutableStateOf(false) }
+    var contactMode by remember { mutableStateOf(false) }
+    DropdownMenu(
+        expanded = expanded,
+        onDismissRequest = { isExpanded(false) },
+    ) {
+        DropdownMenuItem(onClick = { setInfoDrawer(true); isExpanded(false) }) {
+            Text("Make Introduction", fontSize = 14.sp)
+        }
+        DropdownMenuItem(onClick = {}) {
+            Text("Disappearing Messages", fontSize = 14.sp)
+        }
+        DropdownMenuItem(onClick = {}) {
+            Text("Delete all messages", fontSize = 14.sp)
+        }
+        DropdownMenuItem(onClick = { connectionMode = true; isExpanded(false) }) {
+            Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
+                Text("Connections", fontSize = 14.sp, modifier = Modifier.align(Alignment.CenterVertically))
+                Icon(Icons.Filled.ArrowRight, "connections", modifier = Modifier.align(Alignment.CenterVertically))
+            }
+        }
+        DropdownMenuItem(onClick = { contactMode = true; isExpanded(false) }) {
+            Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
+                Text("Contact", fontSize = 14.sp, modifier = Modifier.align(Alignment.CenterVertically))
+                Icon(Icons.Filled.ArrowRight, "connections", modifier = Modifier.align(Alignment.CenterVertically))
+            }
+        }
+    }
+    if (connectionMode) {
+        DropdownMenu(
+            expanded = connectionMode,
+            onDismissRequest = { connectionMode = false },
+        ) {
+            DropdownMenuItem(onClick = { false }) {
+                Text("Connections", fontSize = 12.sp)
+            }
+            DropdownMenuItem(onClick = { false }) {
+                Text("Connect via Bluetooth", fontSize = 14.sp)
+            }
+            DropdownMenuItem(onClick = { false }) {
+                Text("Connect via Removable Device", fontSize = 14.sp)
+            }
+        }
+    }
+    if (contactMode) {
+        DropdownMenu(
+            expanded = contactMode,
+            onDismissRequest = { contactMode = false },
+        ) {
+            DropdownMenuItem(onClick = { false }) {
+                Text("Contact", fontSize = 12.sp)
+            }
+            DropdownMenuItem(onClick = { false }) {
+                Text("Change contact name", fontSize = 14.sp)
+            }
+            DropdownMenuItem(onClick = { false }) {
+                Text("Delete contact", fontSize = 14.sp)
+            }
+        }
+    }
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactInfoDrawer.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactInfoDrawer.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6c8ec6e06e35bb7881610792acfc00140e4a86e3
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactInfoDrawer.kt
@@ -0,0 +1,25 @@
+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.conversation.ContactDrawerMakeIntro
+
+// Right drawer state
+enum class ContactInfoDrawerState {
+    MakeIntro,
+    ConnectBT,
+    ConnectRD
+}
+
+@Composable
+fun ContactInfoDrawer(
+    contact: Contact,
+    contacts: List<Contact>,
+    setInfoDrawer: (Boolean) -> Unit,
+    drawerState: ContactInfoDrawerState
+) {
+    when (drawerState) {
+        MakeIntro -> ContactDrawerMakeIntro(contact, contacts, setInfoDrawer)
+    }
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b25092841f3418706f8a3f02687872c63f96f8a2
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactList.kt
@@ -0,0 +1,61 @@
+package org.briarproject.briar.desktop.conversation
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+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.bramble.api.contact.Contact
+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
+
+@Composable
+fun ContactList(
+    contact: Contact,
+    contacts: List<Contact>,
+    onContactSelect: (Contact) -> Unit,
+    onContactAdd: (Boolean) -> Unit
+) {
+    var searchValue by remember { mutableStateOf("") }
+    val filteredContacts = if (searchValue.isEmpty()) {
+        ArrayList(contacts)
+    } else {
+        val resultList = ArrayList<Contact>()
+        for (c in contacts) {
+            if (c.author.name.lowercase().contains(searchValue.lowercase())) {
+                resultList.add(c)
+            }
+        }
+        resultList
+    }
+    Scaffold(
+        modifier = Modifier.fillMaxHeight().width(CONTACTLIST_WIDTH),
+        backgroundColor = MaterialTheme.colors.surfaceVariant,
+        topBar = {
+            Column(
+                modifier = Modifier.fillMaxWidth().height(HEADER_SIZE + 1.dp),
+            ) {
+                SearchTextField(searchValue, onValueChange = { searchValue = it }, onContactAdd)
+            }
+        },
+        content = {
+            Column(Modifier.verticalScroll(rememberScrollState())) {
+                for (c in filteredContacts) {
+                    ContactCard(c, contact, onContactSelect, contact.id == c.id, true)
+                }
+            }
+        },
+    )
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/dialogs/ContactsViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt
similarity index 83%
rename from src/main/kotlin/org/briarproject/briar/desktop/dialogs/ContactsViewModel.kt
rename to src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt
index 194155a15845287818e8c7197182447571fbbfa4..9e51de185321c436107891c7c371c911423967ed 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/dialogs/ContactsViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactsViewModel.kt
@@ -1,4 +1,4 @@
-package org.briarproject.briar.desktop.dialogs
+package org.briarproject.briar.desktop.contact
 
 import androidx.compose.runtime.mutableStateListOf
 import org.briarproject.bramble.api.contact.Contact
@@ -25,4 +25,8 @@ constructor(
             this.contacts.add(contact)
         }
     }
+
+    fun getFirst(): Contact? {
+        return if (contacts.isEmpty()) null else contacts[0]
+    }
 }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/views/Identicon.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/Identicon.kt
similarity index 98%
rename from src/main/kotlin/org/briarproject/briar/desktop/paul/views/Identicon.kt
rename to src/main/kotlin/org/briarproject/briar/desktop/contact/Identicon.kt
index 30dc74d9adfb84ac92ee978ef97b9ed0881ed40a..4adf14020435ea4de21ae5c48abf2a01528c7075 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/paul/views/Identicon.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/Identicon.kt
@@ -1,4 +1,4 @@
-package org.briarproject.briar.desktop.paul.views
+package org.briarproject.briar.desktop.contact
 
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Size
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/views/ProfileCircle.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ProfileCircle.kt
similarity index 92%
rename from src/main/kotlin/org/briarproject/briar/desktop/paul/views/ProfileCircle.kt
rename to src/main/kotlin/org/briarproject/briar/desktop/contact/ProfileCircle.kt
index c821e0391e0ae0a7ff881c33be0df9e4a76762ee..e2fcbf24336dcbed7ecf24ba62fb52cb931eea22 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/paul/views/ProfileCircle.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ProfileCircle.kt
@@ -1,4 +1,4 @@
-package org.briarproject.briar.desktop.paul.views
+package org.briarproject.briar.desktop.contact
 
 import androidx.compose.desktop.ui.tooling.preview.Preview
 import androidx.compose.foundation.Canvas
@@ -11,7 +11,7 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
-import org.briarproject.briar.desktop.paul.theme.outline
+import org.briarproject.briar.desktop.theme.outline
 
 @Preview
 @Composable
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/SearchTextField.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/SearchTextField.kt
new file mode 100644
index 0000000000000000000000000000000000000000..824ef78bc17d39a113fcf4315fc006530d0475f3
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/SearchTextField.kt
@@ -0,0 +1,48 @@
+package org.briarproject.briar.desktop.conversation
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.TextField
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.PersonAdd
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+@Composable
+fun SearchTextField(searchValue: String, onValueChange: (String) -> Unit, onContactAdd: (Boolean) -> Unit) {
+    TextField(
+        value = searchValue,
+        onValueChange = onValueChange,
+        singleLine = true,
+        textStyle = TextStyle(fontSize = 16.sp, color = MaterialTheme.colors.onSurface),
+        placeholder = { Text("Contacts") },
+        shape = RoundedCornerShape(0.dp),
+        leadingIcon = {
+            val padding = Modifier.padding(top = 8.dp, bottom = 8.dp, start = 12.dp)
+            Icon(Icons.Filled.Search, "search contacts", padding)
+        },
+        trailingIcon = {
+            IconButton(
+                onClick = { onContactAdd(true) },
+                modifier = Modifier.padding(end = 10.dp).size(32.dp)
+                    .background(MaterialTheme.colors.primary, CircleShape)
+            ) {
+                Icon(Icons.Filled.PersonAdd, "add contact", tint = Color.White, modifier = Modifier.size(20.dp))
+            }
+        },
+        modifier = Modifier.fillMaxSize()
+    )
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/chat/Chat.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/Chat.kt
similarity index 74%
rename from src/main/kotlin/org/briarproject/briar/desktop/chat/Chat.kt
rename to src/main/kotlin/org/briarproject/briar/desktop/conversation/Chat.kt
index d59cdeda91fba29968cce179e904dbd74831ad9e..9735b9968873dc9dfeae814371f82bfb4d19562e 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/chat/Chat.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/Chat.kt
@@ -1,6 +1,6 @@
-package org.briarproject.briar.desktop.chat
+package org.briarproject.briar.desktop.conversation
 
-import org.briarproject.briar.desktop.paul.views.getFormattedTimestamp
+import org.briarproject.briar.desktop.utils.TimeUtils.getFormattedTimestamp
 
 class Chat {
 
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/chat/ChatHistoryConversationVisitor.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ChatHistoryConversationVisitor.kt
similarity index 98%
rename from src/main/kotlin/org/briarproject/briar/desktop/chat/ChatHistoryConversationVisitor.kt
rename to src/main/kotlin/org/briarproject/briar/desktop/conversation/ChatHistoryConversationVisitor.kt
index f8aa3835bbea1bca852b8070393882a533453f2d..18f19329c85ad64369b0b21742159346b3872b1d 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/chat/ChatHistoryConversationVisitor.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ChatHistoryConversationVisitor.kt
@@ -1,4 +1,4 @@
-package org.briarproject.briar.desktop.chat
+package org.briarproject.briar.desktop.conversation
 
 import org.briarproject.bramble.api.db.DbException
 import org.briarproject.bramble.util.LogUtils
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ChatState.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ChatState.kt
new file mode 100644
index 0000000000000000000000000000000000000000..316df7dc3116c7ea805560acb00130f50ebff4fd
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ChatState.kt
@@ -0,0 +1,37 @@
+package org.briarproject.briar.desktop.conversation
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import org.briarproject.bramble.api.contact.ContactId
+import org.briarproject.briar.api.conversation.ConversationMessageHeader
+import org.briarproject.briar.desktop.CVM
+import org.briarproject.briar.desktop.MM
+import org.briarproject.briar.desktop.ui.UiState
+import java.util.Collections
+
+@Composable
+fun ChatState(id: ContactId): MutableState<UiState<Chat>> {
+    val state: MutableState<UiState<Chat>> = remember { mutableStateOf(UiState.Loading) }
+    val messagingManager = MM.current
+    val conversationManager = CVM.current
+
+    DisposableEffect(id) {
+        state.value = UiState.Loading
+        val chat = Chat()
+        val visitor = ChatHistoryConversationVisitor(chat, messagingManager)
+        val messageHeaders: List<ConversationMessageHeader> = ArrayList(conversationManager.getMessageHeaders(id))
+        Collections.sort(messageHeaders, ConversationMessageHeaderComparator())
+        // Reverse order here because we're using reverseLayout=true on the LazyColumn to display items
+        // from bottom to top
+        Collections.reverse(messageHeaders)
+        for (header in messageHeaders) {
+            header.accept(visitor)
+        }
+        state.value = UiState.Success(chat)
+        onDispose { }
+    }
+    return state
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/Conversation.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/Conversation.kt
new file mode 100644
index 0000000000000000000000000000000000000000..92c5988d8158b8375ec8a3ca1198a3ba78256a70
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/Conversation.kt
@@ -0,0 +1,78 @@
+package org.briarproject.briar.desktop.conversation
+
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+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.theme.surfaceVariant
+import org.briarproject.briar.desktop.ui.Constants.CONTACTLIST_WIDTH
+import org.briarproject.briar.desktop.views.SIDEBAR_WIDTH
+
+@Composable
+fun Conversation(
+    contact: Contact,
+    contacts: List<Contact>,
+    expanded: Boolean,
+    setExpanded: (Boolean) -> Unit,
+    infoDrawer: Boolean,
+    setInfoDrawer: (Boolean) -> Unit,
+    drawerState: ContactInfoDrawerState
+) {
+    BoxWithConstraints(Modifier.fillMaxSize()) {
+        val animatedInfoDrawerOffsetX by animateDpAsState(if (infoDrawer) (-275).dp else 0.dp)
+        Scaffold(
+            topBar = { ConversationHeader(contact, expanded, setExpanded, setInfoDrawer) },
+            content = { padding ->
+                Box(modifier = Modifier.padding(padding)) {
+                    val chat = ChatState(contact.id)
+                    TextBubbles(chat.value)
+                }
+            },
+            bottomBar = { ConversationInput() },
+        )
+        if (infoDrawer) {
+            // TODO Find non-hacky way of setting scrim on entire app
+            Box(
+                Modifier.offset(-(CONTACTLIST_WIDTH + SIDEBAR_WIDTH))
+                    .requiredSize(maxWidth + CONTACTLIST_WIDTH + SIDEBAR_WIDTH, maxHeight)
+                    .background(Color(0, 0, 0, 100))
+                    .clickable(
+                        // prevent visual indication
+                        interactionSource = remember { MutableInteractionSource() },
+                        indication = null
+                    ) { setInfoDrawer(false) }
+            )
+            Column(
+                modifier = Modifier.fillMaxHeight().width(CONTACTLIST_WIDTH)
+                    .offset(maxWidth + animatedInfoDrawerOffsetX)
+                    .background(
+                        MaterialTheme.colors.surfaceVariant,
+                        RoundedCornerShape(topStart = 10.dp, bottomStart = 10.dp)
+                    )
+            ) {
+                ContactInfoDrawer(contact, contacts, setInfoDrawer, drawerState)
+            }
+        }
+    }
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationHeader.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationHeader.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0372dce4d2e517c30aabefc0bc1bddf211bc6d99
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationHeader.kt
@@ -0,0 +1,66 @@
+package org.briarproject.briar.desktop.conversation
+
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+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.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.MaterialTheme
+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.ui.Alignment
+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.contact.ProfileCircle
+import org.briarproject.briar.desktop.theme.outline
+import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE
+
+@Composable
+fun ConversationHeader(
+    contact: Contact,
+    expanded: Boolean,
+    isExpanded: (Boolean) -> Unit,
+    setInfoDrawer: (Boolean) -> Unit
+) {
+    // TODO hook up online indicator logic
+    val onlineColor = MaterialTheme.colors.secondary
+    val outlineColor = MaterialTheme.colors.outline
+
+    Box(modifier = Modifier.fillMaxWidth().height(HEADER_SIZE + 1.dp)) {
+        Row(modifier = Modifier.align(Alignment.Center)) {
+            ProfileCircle(36.dp, contact.author.id.bytes)
+            Canvas(
+                modifier = Modifier.align(Alignment.CenterVertically),
+                onDraw = {
+                    val size = 10.dp.toPx()
+                    withTransform({ translate(left = -6f, top = 12f) }) {
+                        drawCircle(color = outlineColor, radius = (size + 2.dp.toPx()) / 2f)
+                        drawCircle(color = onlineColor, radius = size / 2f)
+                    }
+                }
+            )
+            Text(
+                contact.author.name,
+                modifier = Modifier.align(Alignment.CenterVertically).padding(start = 12.dp),
+                fontSize = 20.sp
+            )
+        }
+        IconButton(
+            onClick = { isExpanded(!expanded) },
+            modifier = Modifier.align(Alignment.CenterEnd).padding(end = 16.dp)
+        ) {
+            Icon(Icons.Filled.MoreVert, "contact info", modifier = Modifier.size(24.dp))
+            ContactDropDown(expanded, isExpanded, setInfoDrawer)
+        }
+        HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter))
+    }
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationInput.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationInput.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8864a041bc990d4f56fe9a07e406d871f04efdd7
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationInput.kt
@@ -0,0 +1,84 @@
+package org.briarproject.briar.desktop.conversation
+
+import androidx.compose.desktop.ui.tooling.preview.Preview
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.material.TextField
+import androidx.compose.material.TextFieldDefaults
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Send
+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.graphics.Color
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import org.briarproject.briar.desktop.theme.DarkColors
+
+@Preview
+@Composable
+fun PreviewConversationInput() {
+    MaterialTheme(colors = DarkColors) {
+        Surface {
+            ConversationInput()
+        }
+    }
+}
+
+@Composable
+fun ConversationInput() {
+    var text by remember { mutableStateOf("") }
+    Column {
+        HorizontalDivider()
+        TextField(
+            value = text,
+            onValueChange = { text = it },
+            maxLines = 10,
+            textStyle = TextStyle(fontSize = 16.sp, lineHeight = 16.sp),
+            placeholder = { Text("Message") },
+            modifier = Modifier.fillMaxWidth(),
+            shape = RoundedCornerShape(0.dp),
+            colors = TextFieldDefaults.textFieldColors(
+                backgroundColor = MaterialTheme.colors.background,
+                focusedIndicatorColor = MaterialTheme.colors.background,
+                unfocusedIndicatorColor = MaterialTheme.colors.background
+            ),
+            leadingIcon = {
+                IconButton(
+                    onClick = {},
+                    Modifier.padding(4.dp).size(32.dp)
+                        .background(MaterialTheme.colors.primary, CircleShape),
+                ) {
+                    Icon(Icons.Filled.Add, "add attachment", Modifier.size(24.dp), Color.White)
+                }
+            },
+            trailingIcon = {
+                IconButton(
+                    onClick = { }, modifier = Modifier.padding(4.dp).size(32.dp),
+                ) {
+                    Icon(
+                        Icons.Filled.Send,
+                        "send message",
+                        tint = MaterialTheme.colors.secondary,
+                        modifier = Modifier.size(24.dp)
+                    )
+                }
+            }
+        )
+    }
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/chat/ConversationMessageHeaderComparator.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationMessageHeaderComparator.kt
similarity index 86%
rename from src/main/kotlin/org/briarproject/briar/desktop/chat/ConversationMessageHeaderComparator.kt
rename to src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationMessageHeaderComparator.kt
index a2b4b680e183ca5ec573f49661a882b649461a05..1579924735705b1baa24002a27dae53ea947595a 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/chat/ConversationMessageHeaderComparator.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationMessageHeaderComparator.kt
@@ -1,4 +1,4 @@
-package org.briarproject.briar.desktop.chat
+package org.briarproject.briar.desktop.conversation
 
 import org.briarproject.briar.api.conversation.ConversationMessageHeader
 
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageView.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageView.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6ab00cf5605f0812bee06ed7ddc1da94a9a6bced
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/PrivateMessageView.kt
@@ -0,0 +1,41 @@
+package org.briarproject.briar.desktop.conversation
+
+import androidx.compose.foundation.layout.Column
+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.bramble.api.contact.Contact
+import org.briarproject.briar.desktop.contact.ContactInfoDrawerState.MakeIntro
+import org.briarproject.briar.desktop.contact.ContactsViewModel
+
+@Composable
+fun PrivateMessageView(
+    contact: Contact,
+    contacts: ContactsViewModel,
+    onContactSelect: (Contact) -> Unit
+) {
+    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(contact, contacts.contacts, onContactSelect, setDialogVisibility)
+        VerticalDivider()
+        Column(modifier = Modifier.weight(1f).fillMaxHeight()) {
+            Conversation(
+                contact,
+                contacts.contacts,
+                dropdownExpanded,
+                setExpanded,
+                infoDrawer,
+                setInfoDrawer,
+                contactDrawerState
+            )
+        }
+    }
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/chat/SimpleMessage.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/SimpleMessage.kt
similarity index 77%
rename from src/main/kotlin/org/briarproject/briar/desktop/chat/SimpleMessage.kt
rename to src/main/kotlin/org/briarproject/briar/desktop/conversation/SimpleMessage.kt
index cf1f56a4916ba82a5a2f64d7c7c0abfc6788776d..686f1e53151c5bd51b46633ee8fc5df58e97f524 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/chat/SimpleMessage.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/SimpleMessage.kt
@@ -1,4 +1,4 @@
-package org.briarproject.briar.desktop.chat
+package org.briarproject.briar.desktop.conversation
 
 data class SimpleMessage(
     val local: Boolean,
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/TextBubble.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/TextBubble.kt
new file mode 100644
index 0000000000000000000000000000000000000000..543f06ad956b7498b400234da970520bfbadeb63
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/TextBubble.kt
@@ -0,0 +1,67 @@
+package org.briarproject.briar.desktop.conversation
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Card
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.DoneAll
+import androidx.compose.material.icons.filled.Schedule
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import org.briarproject.briar.desktop.theme.awayMsgBubble
+import org.briarproject.briar.desktop.theme.localMsgBubble
+
+@Composable
+fun TextBubble(m: SimpleMessage) {
+    if (m.local) {
+        TextBubble(
+            m,
+            Alignment.End,
+            MaterialTheme.colors.localMsgBubble,
+            RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp, bottomStart = 10.dp)
+        )
+    } else {
+        TextBubble(
+            m,
+            Alignment.Start,
+            MaterialTheme.colors.awayMsgBubble,
+            RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp, bottomEnd = 10.dp)
+        )
+    }
+}
+
+@Composable
+fun TextBubble(m: SimpleMessage, alignment: Alignment.Horizontal, color: Color, shape: RoundedCornerShape) {
+    Column(Modifier.fillMaxWidth()) {
+        Column(Modifier.fillMaxWidth(fraction = 0.8f).align(alignment)) {
+            Card(Modifier.align(alignment), backgroundColor = color, shape = shape) {
+                Column(
+                    Modifier.padding(8.dp)
+                ) {
+                    Text(m.message, fontSize = 14.sp, modifier = Modifier.align(Alignment.Start))
+                    Row(modifier = Modifier.padding(top = 4.dp)) {
+                        Text(m.time, Modifier.padding(end = 4.dp), fontSize = 10.sp)
+                        if (m.delivered) {
+                            val modifier = Modifier.size(12.dp).align(Alignment.CenterVertically)
+                            Icon(Icons.Filled.DoneAll, "sent", modifier)
+                        } else {
+                            val modifier = Modifier.size(12.dp).align(Alignment.CenterVertically)
+                            Icon(Icons.Filled.Schedule, "sending", modifier)
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/TextBubbles.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/TextBubbles.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d8e157a418821fc6316cd600d67c92196c72c6bc
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/TextBubbles.kt
@@ -0,0 +1,25 @@
+package org.briarproject.briar.desktop.conversation
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.dp
+import org.briarproject.briar.desktop.ui.UiState
+
+@Composable
+fun TextBubbles(chat: UiState<Chat>) {
+    when (chat) {
+        is UiState.Loading -> Loader()
+        is UiState.Error -> Loader()
+        is UiState.Success ->
+            LazyColumn(
+                verticalArrangement = Arrangement.spacedBy(8.dp),
+                reverseLayout = true,
+                contentPadding = PaddingValues(8.dp)
+            ) {
+                items(chat.data.messages) { m -> TextBubble(m) }
+            }
+    }
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/dialogs/Login.kt b/src/main/kotlin/org/briarproject/briar/desktop/login/Login.kt
similarity index 98%
rename from src/main/kotlin/org/briarproject/briar/desktop/dialogs/Login.kt
rename to src/main/kotlin/org/briarproject/briar/desktop/login/Login.kt
index a61f33837cd4a0e951f45f518c39992595e0eb5c..488a225e8a53532798573f64f6b77d06ff4731f6 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/dialogs/Login.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/login/Login.kt
@@ -1,4 +1,4 @@
-package org.briarproject.briar.desktop.dialogs
+package org.briarproject.briar.desktop.login
 
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.layout.Arrangement
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/dialogs/LoginViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/login/LoginViewModel.kt
similarity index 95%
rename from src/main/kotlin/org/briarproject/briar/desktop/dialogs/LoginViewModel.kt
rename to src/main/kotlin/org/briarproject/briar/desktop/login/LoginViewModel.kt
index 228a8012197763b092fd5c0e3fe213dcf0f382df..a86e37f42534991545ca1340499d7ceb96547855 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/dialogs/LoginViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/login/LoginViewModel.kt
@@ -1,4 +1,4 @@
-package org.briarproject.briar.desktop.dialogs
+package org.briarproject.briar.desktop.login
 
 import androidx.compose.runtime.State
 import androidx.compose.runtime.mutableStateOf
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/dialogs/Registration.kt b/src/main/kotlin/org/briarproject/briar/desktop/login/Registration.kt
similarity index 98%
rename from src/main/kotlin/org/briarproject/briar/desktop/dialogs/Registration.kt
rename to src/main/kotlin/org/briarproject/briar/desktop/login/Registration.kt
index 7cc06944c6aa53ab3f8b69aad27363c6248750db..78758000be6c01d520982cb4c7569d856ecb5f03 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/dialogs/Registration.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/login/Registration.kt
@@ -1,4 +1,4 @@
-package org.briarproject.briar.desktop.dialogs
+package org.briarproject.briar.desktop.login
 
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/dialogs/RegistrationViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/login/RegistrationViewModel.kt
similarity index 96%
rename from src/main/kotlin/org/briarproject/briar/desktop/dialogs/RegistrationViewModel.kt
rename to src/main/kotlin/org/briarproject/briar/desktop/login/RegistrationViewModel.kt
index b5a9b016e02a2ef18ee3b383285bc749e2ea60e6..6a133b40d2e7bf54030140735869042633e57227 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/dialogs/RegistrationViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/login/RegistrationViewModel.kt
@@ -1,4 +1,4 @@
-package org.briarproject.briar.desktop.dialogs
+package org.briarproject.briar.desktop.login
 
 import androidx.compose.runtime.State
 import androidx.compose.runtime.mutableStateOf
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/views/BriarSidebar.kt b/src/main/kotlin/org/briarproject/briar/desktop/navigation/BriarSidebar.kt
similarity index 84%
rename from src/main/kotlin/org/briarproject/briar/desktop/paul/views/BriarSidebar.kt
rename to src/main/kotlin/org/briarproject/briar/desktop/navigation/BriarSidebar.kt
index 007b84da5604bb8138c639764c9decb4677d1bb8..f91bcf5008fd3bcf48bcd400d9ce2557227a7f11 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/paul/views/BriarSidebar.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/navigation/BriarSidebar.kt
@@ -1,4 +1,4 @@
-package org.briarproject.briar.desktop.paul.views
+package org.briarproject.briar.desktop.views
 
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
@@ -24,12 +24,14 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.unit.dp
 import org.briarproject.briar.desktop.IM
-import org.briarproject.briar.desktop.paul.theme.sidebarSurface
+import org.briarproject.briar.desktop.contact.ProfileCircle
+import org.briarproject.briar.desktop.theme.sidebarSurface
+import org.briarproject.briar.desktop.ui.UiMode
 
 val SIDEBAR_WIDTH = 56.dp
 
 @Composable
-fun BriarSidebar(uiMode: UiModes, setUiMode: (UiModes) -> Unit) {
+fun BriarSidebar(uiMode: UiMode, setUiMode: (UiMode) -> Unit) {
     Surface(modifier = Modifier.width(SIDEBAR_WIDTH).fillMaxHeight(), color = MaterialTheme.colors.sidebarSurface) {
         Column(verticalArrangement = Arrangement.Top) {
             IconButton(
@@ -42,25 +44,25 @@ fun BriarSidebar(uiMode: UiModes, setUiMode: (UiModes) -> Unit) {
             BriarSidebarButton(
                 uiMode = uiMode,
                 setUiMode = setUiMode,
-                UiModes.CONTACTS,
+                UiMode.CONTACTS,
                 Icons.Filled.Contacts
             )
             BriarSidebarButton(
                 uiMode = uiMode,
                 setUiMode = setUiMode,
-                UiModes.GROUPS,
+                UiMode.GROUPS,
                 Icons.Filled.Group
             )
             BriarSidebarButton(
                 uiMode = uiMode,
                 setUiMode = setUiMode,
-                UiModes.FORUMS,
+                UiMode.FORUMS,
                 Icons.Filled.Forum
             )
             BriarSidebarButton(
                 uiMode = uiMode,
                 setUiMode = setUiMode,
-                UiModes.BLOGS,
+                UiMode.BLOGS,
                 Icons.Filled.ChromeReaderMode
             )
         }
@@ -68,19 +70,19 @@ fun BriarSidebar(uiMode: UiModes, setUiMode: (UiModes) -> Unit) {
             BriarSidebarButton(
                 uiMode = uiMode,
                 setUiMode = setUiMode,
-                UiModes.TRANSPORTS,
+                UiMode.TRANSPORTS,
                 Icons.Filled.WifiTethering
             )
             BriarSidebarButton(
                 uiMode = uiMode,
                 setUiMode = setUiMode,
-                UiModes.SETTINGS,
+                UiMode.SETTINGS,
                 Icons.Filled.Settings
             )
             BriarSidebarButton(
                 uiMode = uiMode,
                 setUiMode = setUiMode,
-                UiModes.SIGNOUT,
+                UiMode.SIGNOUT,
                 Icons.Filled.Logout
             )
         }
@@ -88,7 +90,7 @@ fun BriarSidebar(uiMode: UiModes, setUiMode: (UiModes) -> Unit) {
 }
 
 @Composable
-fun BriarSidebarButton(uiMode: UiModes, setUiMode: (UiModes) -> Unit, thisMode: UiModes, icon: ImageVector) {
+fun BriarSidebarButton(uiMode: UiMode, setUiMode: (UiMode) -> Unit, thisMode: UiMode, icon: ImageVector) {
     val tint = if (uiMode == thisMode) MaterialTheme.colors.primary else MaterialTheme.colors.onSurface
     Column {
         IconButton(
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/data/ContentTypeList.kt b/src/main/kotlin/org/briarproject/briar/desktop/paul/data/ContentTypeList.kt
deleted file mode 100644
index 66f0de30cdfccc89db20096dc2a1f0875e75239a..0000000000000000000000000000000000000000
--- a/src/main/kotlin/org/briarproject/briar/desktop/paul/data/ContentTypeList.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package org.briarproject.briar.desktop.paul.data
-
-import org.briarproject.briar.desktop.paul.model.ContentType
-
-object ContentTypeList {
-    val types = listOf(
-        ContentType(id = -1, name = "Profile"),
-        ContentType(id = 0, name = "Contacts"),
-        ContentType(id = 1, name = "Private Groups"),
-        ContentType(id = 2, name = "Forums"),
-        ContentType(id = 3, name = "Blogs"),
-        ContentType(id = 4, name = "Transports"),
-        ContentType(id = 5, name = "Settings"),
-        ContentType(id = 6, name = "Sign Out")
-    )
-}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/data/OptionTypeList.kt b/src/main/kotlin/org/briarproject/briar/desktop/paul/data/OptionTypeList.kt
deleted file mode 100644
index b8e60c48a9071b7d714d7d04237942f248d4e386..0000000000000000000000000000000000000000
--- a/src/main/kotlin/org/briarproject/briar/desktop/paul/data/OptionTypeList.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.briarproject.briar.desktop.paul.data
-
-import org.briarproject.briar.desktop.paul.model.OptionType
-
-object OptionTypeList {
-    val msgTypes = listOf(
-        OptionType(name = "Alice", unread = 2, online = true),
-        OptionType(name = "Bob", unread = 0, online = true),
-        OptionType(name = "Carl", unread = 0, online = false),
-        OptionType(name = "Dan", unread = 1, online = false),
-        OptionType(name = "Eve", unread = 0, online = false),
-    )
-}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/model/ContentType.kt b/src/main/kotlin/org/briarproject/briar/desktop/paul/model/ContentType.kt
deleted file mode 100644
index 0704b6c3f174425bf5fc4ef05755a423efbddf68..0000000000000000000000000000000000000000
--- a/src/main/kotlin/org/briarproject/briar/desktop/paul/model/ContentType.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package org.briarproject.briar.desktop.paul.model
-
-data class ContentType(
-    val id: Int,
-    val name: String,
-    // val icon: Icon,
-)
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/model/OptionType.kt b/src/main/kotlin/org/briarproject/briar/desktop/paul/model/OptionType.kt
deleted file mode 100644
index ae5b2e4d76b5da4b0cf655a4e3424342ae6e22f6..0000000000000000000000000000000000000000
--- a/src/main/kotlin/org/briarproject/briar/desktop/paul/model/OptionType.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package org.briarproject.briar.desktop.paul.model
-
-data class OptionType(
-    val name: String,
-    val unread: Int,
-    val online: Boolean
-    // val icon: Icon,
-)
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/views/PrivateMessageView.kt b/src/main/kotlin/org/briarproject/briar/desktop/paul/views/PrivateMessageView.kt
deleted file mode 100644
index a05d9cae64e0d171be6911dda7ef1e62d8443634..0000000000000000000000000000000000000000
--- a/src/main/kotlin/org/briarproject/briar/desktop/paul/views/PrivateMessageView.kt
+++ /dev/null
@@ -1,802 +0,0 @@
-package org.briarproject.briar.desktop.paul.views
-
-import androidx.compose.animation.core.animateDpAsState
-import androidx.compose.desktop.ui.tooling.preview.Preview
-import androidx.compose.foundation.Canvas
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxWithConstraints
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.offset
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.AlertDialog
-import androidx.compose.material.Button
-import androidx.compose.material.Card
-import androidx.compose.material.CircularProgressIndicator
-import androidx.compose.material.Divider
-import androidx.compose.material.DropdownMenu
-import androidx.compose.material.DropdownMenuItem
-import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.Icon
-import androidx.compose.material.IconButton
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Scaffold
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
-import androidx.compose.material.TextButton
-import androidx.compose.material.TextField
-import androidx.compose.material.TextFieldDefaults
-import androidx.compose.material.icons.Icons.Filled
-import androidx.compose.material.icons.filled.Add
-import androidx.compose.material.icons.filled.ArrowBack
-import androidx.compose.material.icons.filled.ArrowRight
-import androidx.compose.material.icons.filled.Close
-import androidx.compose.material.icons.filled.DoneAll
-import androidx.compose.material.icons.filled.MoreVert
-import androidx.compose.material.icons.filled.PersonAdd
-import androidx.compose.material.icons.filled.Schedule
-import androidx.compose.material.icons.filled.Search
-import androidx.compose.material.icons.filled.Send
-import androidx.compose.material.icons.filled.SwapHoriz
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.MutableState
-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.graphics.drawscope.withTransform
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.input.TextFieldValue
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import org.briarproject.bramble.api.FormatException
-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.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.ConversationMessageHeader
-import org.briarproject.briar.desktop.CTM
-import org.briarproject.briar.desktop.CVM
-import org.briarproject.briar.desktop.MM
-import org.briarproject.briar.desktop.chat.Chat
-import org.briarproject.briar.desktop.chat.ChatHistoryConversationVisitor
-import org.briarproject.briar.desktop.chat.ConversationMessageHeaderComparator
-import org.briarproject.briar.desktop.chat.SimpleMessage
-import org.briarproject.briar.desktop.chat.UiState
-import org.briarproject.briar.desktop.dialogs.ContactsViewModel
-import org.briarproject.briar.desktop.paul.theme.DarkColors
-import org.briarproject.briar.desktop.paul.theme.awayMsgBubble
-import org.briarproject.briar.desktop.paul.theme.divider
-import org.briarproject.briar.desktop.paul.theme.localMsgBubble
-import org.briarproject.briar.desktop.paul.theme.outline
-import org.briarproject.briar.desktop.paul.theme.selectedCard
-import org.briarproject.briar.desktop.paul.theme.surfaceVariant
-import java.security.GeneralSecurityException
-import java.time.Instant
-import java.time.LocalDateTime
-import java.time.ZoneId
-import java.time.format.DateTimeFormatter
-import java.time.format.FormatStyle
-import java.time.temporal.ChronoUnit
-import java.util.Collections
-
-val HEADER_SIZE = 56.dp
-val CONTACTLIST_WIDTH = 275.dp
-
-// Right drawer state
-enum class ContactInfoDrawerState {
-    MakeIntro,
-    ConnectBT,
-    ConnectRD
-}
-
-@Composable
-fun HorizontalDivider(modifier: Modifier = Modifier) {
-    Divider(color = MaterialTheme.colors.divider, thickness = 1.dp, modifier = modifier.fillMaxWidth())
-}
-
-@Composable
-fun VerticalDivider() {
-    Divider(color = MaterialTheme.colors.divider, modifier = Modifier.fillMaxHeight().width(1.dp))
-}
-
-@Composable
-fun PrivateMessageView(
-    contact: Contact,
-    contacts: ContactsViewModel,
-    onContactSelect: (Contact) -> Unit
-) {
-    val (isDialogVisible, setDialogVisibility) = remember { mutableStateOf(false) }
-    val (dropdownExpanded, setExpanded) = remember { mutableStateOf(false) }
-    val (infoDrawer, setInfoDrawer) = remember { mutableStateOf(false) }
-    val (contactDrawerState, setDrawerState) = remember { mutableStateOf(ContactInfoDrawerState.MakeIntro) }
-    AddContactDialog(isDialogVisible, setDialogVisibility)
-    Row(modifier = Modifier.fillMaxWidth()) {
-        ContactList(contact, contacts.contacts, onContactSelect, setDialogVisibility)
-        VerticalDivider()
-        Column(modifier = Modifier.weight(1f).fillMaxHeight()) {
-            Conversation(
-                contact,
-                contacts.contacts,
-                dropdownExpanded,
-                setExpanded,
-                infoDrawer,
-                setInfoDrawer,
-                contactDrawerState
-            )
-        }
-    }
-}
-
-@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
-    AlertDialog(
-        onDismissRequest = { setDialogVisibility(false) },
-        text = {
-            Column(modifier = Modifier.fillMaxWidth()) {
-                Row(Modifier.fillMaxWidth().padding(vertical = 16.dp)) {
-                    Text(
-                        text = "Add Contact at a Distance",
-                        fontSize = 24.sp,
-                        modifier = Modifier.align(Alignment.CenterVertically)
-                    )
-                }
-                Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
-                    Text(
-                        "Contact's Link",
-                        Modifier.width(128.dp).align(Alignment.CenterVertically),
-                    )
-                    TextField(contactLink, onValueChange = { contactLink = it }, 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())
-                }
-                Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
-                    Text(
-                        "Your Link",
-                        modifier = Modifier.width(128.dp).align(Alignment.CenterVertically),
-                    )
-                    TextField(
-                        ownLink,
-                        onValueChange = {},
-                        modifier = Modifier.fillMaxWidth()
-                    )
-                }
-            }
-        },
-        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)
-                },
-            ) {
-                Text("Add")
-            }
-        },
-        dismissButton = {
-            TextButton(
-                onClick = { setDialogVisibility(false) }
-            ) {
-                Text("Cancel", color = MaterialTheme.colors.onSurface)
-            }
-        },
-        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
-}
-
-@Composable
-fun SearchTextField(searchValue: String, onValueChange: (String) -> Unit, onContactAdd: (Boolean) -> Unit) {
-    TextField(
-        value = searchValue,
-        onValueChange = onValueChange,
-        singleLine = true,
-        textStyle = TextStyle(fontSize = 16.sp, color = MaterialTheme.colors.onSurface),
-        placeholder = { Text("Contacts") },
-        shape = RoundedCornerShape(0.dp),
-        leadingIcon = {
-            val padding = Modifier.padding(top = 8.dp, bottom = 8.dp, start = 12.dp)
-            Icon(Filled.Search, "search contacts", padding)
-        },
-        trailingIcon = {
-            IconButton(
-                onClick = { onContactAdd(true) },
-                modifier = Modifier.padding(end = 10.dp).size(32.dp)
-                    .background(MaterialTheme.colors.primary, CircleShape)
-            ) {
-                Icon(Filled.PersonAdd, "add contact", tint = Color.White, modifier = Modifier.size(20.dp))
-            }
-        },
-        modifier = Modifier.fillMaxSize()
-    )
-}
-
-@Composable
-fun ContactCard(
-    contact: Contact,
-    selContact: Contact,
-    onSel: (Contact) -> Unit,
-    selected: Boolean,
-    drawNotifications: Boolean
-) {
-    val bgColor = if (selected) MaterialTheme.colors.selectedCard else MaterialTheme.colors.surfaceVariant
-    val outlineColor = MaterialTheme.colors.outline
-    val briarPrimary = MaterialTheme.colors.primary
-    val briarSecondary = MaterialTheme.colors.secondary
-    val briarSurfaceVar = MaterialTheme.colors.surfaceVariant
-
-    Card(
-        modifier = Modifier.fillMaxWidth().height(HEADER_SIZE).clickable(onClick = { onSel(contact) }),
-        shape = RoundedCornerShape(0.dp),
-        backgroundColor = bgColor,
-        contentColor = MaterialTheme.colors.onSurface
-    ) {
-        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)
-                // Draw notification badges
-                if (drawNotifications) {
-                    Canvas(
-                        modifier = Modifier.align(Alignment.CenterVertically),
-                        onDraw = {
-                            val size = 10.dp.toPx()
-                            withTransform({ translate(left = -6f, top = -12f) }) {
-                                drawCircle(color = outlineColor, radius = (size + 2.dp.toPx()) / 2f)
-                                drawCircle(color = briarPrimary, radius = size / 2f)
-                            }
-                        }
-                    )
-                }
-                Column(modifier = Modifier.align(Alignment.CenterVertically).padding(start = 12.dp)) {
-                    Text(
-                        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),
-                        fontSize = 10.sp,
-                        modifier = Modifier.align(Alignment.Start)
-                    )
-                }
-            }
-            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)
-                    }
-                }
-            )
-        }
-    }
-    HorizontalDivider()
-}
-
-fun getFormattedTimestamp(timestamp: Long): String {
-    val messageTime = LocalDateTime.ofInstant(
-        Instant.ofEpochMilli(timestamp), ZoneId.systemDefault()
-    )
-    val currentTime = LocalDateTime.now()
-    val difference = ChronoUnit.MINUTES.between(messageTime, currentTime)
-
-    val formatter = if (difference < 1440) { // = 1 day
-        DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
-    } else {
-        DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT)
-    }
-    return messageTime.format(formatter)
-}
-
-@Composable
-fun ContactList(
-    contact: Contact,
-    contacts: List<Contact>,
-    onContactSelect: (Contact) -> Unit,
-    onContactAdd: (Boolean) -> Unit
-) {
-    var searchValue by remember { mutableStateOf("") }
-    val filteredContacts = if (searchValue.isEmpty()) {
-        ArrayList(contacts)
-    } else {
-        val resultList = ArrayList<Contact>()
-        for (c in contacts) {
-            if (c.author.name.lowercase().contains(searchValue.lowercase())) {
-                resultList.add(c)
-            }
-        }
-        resultList
-    }
-    Scaffold(
-        modifier = Modifier.fillMaxHeight().width(CONTACTLIST_WIDTH),
-        backgroundColor = MaterialTheme.colors.surfaceVariant,
-        topBar = {
-            Column(
-                modifier = Modifier.fillMaxWidth().height(HEADER_SIZE + 1.dp),
-            ) {
-                SearchTextField(searchValue, onValueChange = { searchValue = it }, onContactAdd)
-            }
-        },
-        content = {
-            Column(Modifier.verticalScroll(rememberScrollState())) {
-                for (c in filteredContacts) {
-                    ContactCard(c, contact, onContactSelect, contact.id == c.id, true)
-                }
-            }
-        },
-    )
-}
-
-@Composable
-fun TextBubble(m: SimpleMessage) {
-    if (m.local) {
-        TextBubble(
-            m,
-            Alignment.End,
-            MaterialTheme.colors.localMsgBubble,
-            RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp, bottomStart = 10.dp)
-        )
-    } else {
-        TextBubble(
-            m,
-            Alignment.Start,
-            MaterialTheme.colors.awayMsgBubble,
-            RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp, bottomEnd = 10.dp)
-        )
-    }
-}
-
-@Composable
-fun TextBubble(m: SimpleMessage, alignment: Alignment.Horizontal, color: Color, shape: RoundedCornerShape) {
-    Column(Modifier.fillMaxWidth()) {
-        Column(Modifier.fillMaxWidth(fraction = 0.8f).align(alignment)) {
-            Card(Modifier.align(alignment), backgroundColor = color, shape = shape) {
-                Column(
-                    Modifier.padding(8.dp)
-                ) {
-                    Text(m.message, fontSize = 14.sp, modifier = Modifier.align(Alignment.Start))
-                    Row(modifier = Modifier.padding(top = 4.dp)) {
-                        Text(m.time, Modifier.padding(end = 4.dp), fontSize = 10.sp)
-                        if (m.delivered) {
-                            val modifier = Modifier.size(12.dp).align(Alignment.CenterVertically)
-                            Icon(Filled.DoneAll, "sent", modifier)
-                        } else {
-                            val modifier = Modifier.size(12.dp).align(Alignment.CenterVertically)
-                            Icon(Filled.Schedule, "sending", modifier)
-                        }
-                    }
-                }
-            }
-        }
-    }
-}
-
-@Composable
-fun DrawTextBubbles(chat: UiState<Chat>) {
-    when (chat) {
-        is UiState.Loading -> Loader()
-        is UiState.Error -> Loader()
-        is UiState.Success ->
-            LazyColumn(
-                verticalArrangement = Arrangement.spacedBy(8.dp),
-                reverseLayout = true,
-                contentPadding = PaddingValues(8.dp)
-            ) {
-                items(chat.data.messages) { m -> TextBubble(m) }
-            }
-    }
-}
-
-@Composable
-fun Loader() {
-    Box(
-        contentAlignment = Alignment.Center,
-        modifier = Modifier.fillMaxWidth().padding(20.dp)
-    ) {
-        CircularProgressIndicator()
-    }
-}
-
-@Composable
-fun ContactDropDown(
-    expanded: Boolean,
-    isExpanded: (Boolean) -> Unit,
-    setInfoDrawer: (Boolean) -> Unit
-) {
-    var connectionMode by remember { mutableStateOf(false) }
-    var contactMode by remember { mutableStateOf(false) }
-    DropdownMenu(
-        expanded = expanded,
-        onDismissRequest = { isExpanded(false) },
-    ) {
-        DropdownMenuItem(onClick = { setInfoDrawer(true); isExpanded(false) }) {
-            Text("Make Introduction", fontSize = 14.sp)
-        }
-        DropdownMenuItem(onClick = {}) {
-            Text("Disappearing Messages", fontSize = 14.sp)
-        }
-        DropdownMenuItem(onClick = {}) {
-            Text("Delete all messages", fontSize = 14.sp)
-        }
-        DropdownMenuItem(onClick = { connectionMode = true; isExpanded(false) }) {
-            Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
-                Text("Connections", fontSize = 14.sp, modifier = Modifier.align(Alignment.CenterVertically))
-                Icon(Filled.ArrowRight, "connections", modifier = Modifier.align(Alignment.CenterVertically))
-            }
-        }
-        DropdownMenuItem(onClick = { contactMode = true; isExpanded(false) }) {
-            Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
-                Text("Contact", fontSize = 14.sp, modifier = Modifier.align(Alignment.CenterVertically))
-                Icon(Filled.ArrowRight, "connections", modifier = Modifier.align(Alignment.CenterVertically))
-            }
-        }
-    }
-    if (connectionMode) {
-        DropdownMenu(
-            expanded = connectionMode,
-            onDismissRequest = { connectionMode = false },
-        ) {
-            DropdownMenuItem(onClick = { false }) {
-                Text("Connections", fontSize = 12.sp)
-            }
-            DropdownMenuItem(onClick = { false }) {
-                Text("Connect via Bluetooth", fontSize = 14.sp)
-            }
-            DropdownMenuItem(onClick = { false }) {
-                Text("Connect via Removable Device", fontSize = 14.sp)
-            }
-        }
-    }
-    if (contactMode) {
-        DropdownMenu(
-            expanded = contactMode,
-            onDismissRequest = { contactMode = false },
-        ) {
-            DropdownMenuItem(onClick = { false }) {
-                Text("Contact", fontSize = 12.sp)
-            }
-            DropdownMenuItem(onClick = { false }) {
-                Text("Change contact name", fontSize = 14.sp)
-            }
-            DropdownMenuItem(onClick = { false }) {
-                Text("Delete contact", fontSize = 14.sp)
-            }
-        }
-    }
-}
-
-@Composable
-fun ConversationHeader(
-    contact: Contact,
-    expanded: Boolean,
-    isExpanded: (Boolean) -> Unit,
-    setInfoDrawer: (Boolean) -> Unit
-) {
-    // TODO hook up online indicator logic
-    val onlineColor = MaterialTheme.colors.secondary
-    val outlineColor = MaterialTheme.colors.outline
-
-    Box(modifier = Modifier.fillMaxWidth().height(HEADER_SIZE + 1.dp)) {
-        Row(modifier = Modifier.align(Alignment.Center)) {
-            ProfileCircle(36.dp, contact.author.id.bytes)
-            Canvas(
-                modifier = Modifier.align(Alignment.CenterVertically),
-                onDraw = {
-                    val size = 10.dp.toPx()
-                    withTransform({ translate(left = -6f, top = 12f) }) {
-                        drawCircle(color = outlineColor, radius = (size + 2.dp.toPx()) / 2f)
-                        drawCircle(color = onlineColor, radius = size / 2f)
-                    }
-                }
-            )
-            Text(
-                contact.author.name,
-                modifier = Modifier.align(Alignment.CenterVertically).padding(start = 12.dp),
-                fontSize = 20.sp
-            )
-        }
-        IconButton(
-            onClick = { isExpanded(!expanded) },
-            modifier = Modifier.align(Alignment.CenterEnd).padding(end = 16.dp)
-        ) {
-            Icon(Filled.MoreVert, "contact info", modifier = Modifier.size(24.dp))
-            ContactDropDown(expanded, isExpanded, setInfoDrawer)
-        }
-        HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter))
-    }
-}
-
-@Composable
-fun ConversationInput() {
-    var text by remember { mutableStateOf("") }
-    Column {
-        HorizontalDivider()
-        TextField(
-            value = text,
-            onValueChange = { text = it },
-            maxLines = 10,
-            textStyle = TextStyle(fontSize = 16.sp, lineHeight = 16.sp),
-            placeholder = { Text("Message") },
-            modifier = Modifier.fillMaxWidth(),
-            shape = RoundedCornerShape(0.dp),
-            colors = TextFieldDefaults.textFieldColors(
-                backgroundColor = MaterialTheme.colors.background,
-                focusedIndicatorColor = MaterialTheme.colors.background,
-                unfocusedIndicatorColor = MaterialTheme.colors.background
-            ),
-            leadingIcon = {
-                IconButton(
-                    onClick = {},
-                    Modifier.padding(4.dp).size(32.dp)
-                        .background(MaterialTheme.colors.primary, CircleShape),
-                ) {
-                    Icon(Filled.Add, "add attachment", Modifier.size(24.dp), Color.White)
-                }
-            },
-            trailingIcon = {
-                IconButton(
-                    onClick = { }, modifier = Modifier.padding(4.dp).size(32.dp),
-                ) {
-                    Icon(
-                        Filled.Send,
-                        "send message",
-                        tint = MaterialTheme.colors.secondary,
-                        modifier = Modifier.size(24.dp)
-                    )
-                }
-            }
-        )
-    }
-}
-
-@Preview
-@Composable
-fun PreviewMsgInput() {
-    MaterialTheme(colors = DarkColors) {
-        Surface {
-            ConversationInput()
-        }
-    }
-}
-
-@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) {
-        Surface {
-            Row(Modifier.fillMaxWidth().height(HEADER_SIZE)) {
-                IconButton(
-                    onClick = { setInfoDrawer(false) },
-                    Modifier.padding(horizontal = 11.dp).size(32.dp).align(Alignment.CenterVertically)
-                ) {
-                    Icon(Filled.Close, "close make intro screen")
-                }
-                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) {
-                        ContactCard(c, contact, { onCancelSel(c); introNextPg = true }, false, false)
-                    }
-                }
-            }
-        }
-    } else {
-        Column {
-            Row(Modifier.fillMaxWidth().height(HEADER_SIZE)) {
-                IconButton(
-                    onClick = { introNextPg = false },
-                    Modifier.padding(horizontal = 11.dp).size(32.dp).align(Alignment.CenterVertically)
-                ) {
-                    Icon(Filled.ArrowBack, "go back to make intro contact screen", tint = Color.White)
-                }
-                Text(
-                    text = "Introduce Contacts",
-                    fontSize = 16.sp,
-                    modifier = Modifier.align(Alignment.CenterVertically)
-                )
-            }
-            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)
-                }
-                Icon(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)
-                }
-            }
-            var introText by remember { mutableStateOf(TextFieldValue("")) }
-            Row(Modifier.padding(8.dp)) {
-                TextField(
-                    introText,
-                    { introText = it },
-                    placeholder = { Text(text = "Add a message (optional)") },
-                )
-            }
-            Row(Modifier.padding(8.dp)) {
-                TextButton(
-                    onClick = { setInfoDrawer(false); introNextPg = false; },
-                    Modifier.fillMaxWidth()
-                ) {
-                    Text("MAKE INTRODUCTION")
-                }
-            }
-        }
-    }
-}
-
-@Composable
-fun ContactInfoDrawer(
-    contact: Contact,
-    contacts: List<Contact>,
-    setInfoDrawer: (Boolean) -> Unit,
-    drawerState: ContactInfoDrawerState
-) {
-    when (drawerState) {
-        ContactInfoDrawerState.MakeIntro -> ContactDrawerMakeIntro(contact, contacts, setInfoDrawer)
-    }
-}
-
-@Composable
-fun Conversation(
-    contact: Contact,
-    contacts: List<Contact>,
-    expanded: Boolean,
-    setExpanded: (Boolean) -> Unit,
-    infoDrawer: Boolean,
-    setInfoDrawer: (Boolean) -> Unit,
-    drawerState: ContactInfoDrawerState
-) {
-    BoxWithConstraints(Modifier.fillMaxSize()) {
-        val animatedInfoDrawerOffsetX by animateDpAsState(if (infoDrawer) (-275).dp else 0.dp)
-        Scaffold(
-            topBar = { ConversationHeader(contact, expanded, setExpanded, setInfoDrawer) },
-            content = { padding ->
-                Box(modifier = Modifier.padding(padding)) {
-                    val chat = ChatState(contact.id)
-                    DrawTextBubbles(chat.value)
-                }
-            },
-            bottomBar = { ConversationInput() },
-        )
-        if (infoDrawer) {
-            // TODO Find non-hacky way of setting scrim on entire app
-            Box(
-                Modifier.offset(-(CONTACTLIST_WIDTH + SIDEBAR_WIDTH))
-                    .requiredSize(maxWidth + CONTACTLIST_WIDTH + SIDEBAR_WIDTH, maxHeight)
-                    .background(Color(0, 0, 0, 100))
-                    .clickable(
-                        // prevent visual indication
-                        interactionSource = remember { MutableInteractionSource() },
-                        indication = null
-                    ) { setInfoDrawer(false) }
-            )
-            Column(
-                modifier = Modifier.fillMaxHeight().width(CONTACTLIST_WIDTH)
-                    .offset(maxWidth + animatedInfoDrawerOffsetX)
-                    .background(
-                        MaterialTheme.colors.surfaceVariant,
-                        RoundedCornerShape(topStart = 10.dp, bottomStart = 10.dp)
-                    )
-            ) {
-                ContactInfoDrawer(contact, contacts, setInfoDrawer, drawerState)
-            }
-        }
-    }
-}
-
-@Composable
-fun ChatState(id: ContactId): MutableState<UiState<Chat>> {
-    val state: MutableState<UiState<Chat>> = remember { mutableStateOf(UiState.Loading) }
-    val messagingManager = MM.current
-    val conversationManager = CVM.current
-
-    DisposableEffect(id) {
-        state.value = UiState.Loading
-        val chat = Chat()
-        val visitor = ChatHistoryConversationVisitor(chat, messagingManager)
-        val messageHeaders: List<ConversationMessageHeader> = ArrayList(conversationManager.getMessageHeaders(id))
-        Collections.sort(messageHeaders, ConversationMessageHeaderComparator())
-        // Reverse order here because we're using reverseLayout=true on the LazyColumn to display items
-        // from bottom to top
-        Collections.reverse(messageHeaders)
-        for (header in messageHeaders) {
-            header.accept(visitor)
-        }
-        state.value = UiState.Success(chat)
-        onDispose { }
-    }
-    return state
-}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/settings/PlaceHolderSettingsView.kt b/src/main/kotlin/org/briarproject/briar/desktop/settings/PlaceHolderSettingsView.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7ad692bb97924fc74714c15ccefda2344db6836d
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/settings/PlaceHolderSettingsView.kt
@@ -0,0 +1,20 @@
+package org.briarproject.briar.desktop.settings
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.Button
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+
+@Composable
+fun PlaceHolderSettingsView(isDark: Boolean, setDark: (Boolean) -> Unit) {
+    Surface(Modifier.fillMaxSize()) {
+        Box {
+            Button(onClick = { setDark(!isDark) }) {
+                Text("Change Theme")
+            }
+        }
+    }
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/theme/Colors.kt b/src/main/kotlin/org/briarproject/briar/desktop/theme/Colors.kt
similarity index 92%
rename from src/main/kotlin/org/briarproject/briar/desktop/paul/theme/Colors.kt
rename to src/main/kotlin/org/briarproject/briar/desktop/theme/Colors.kt
index ec1908c844e0ef3586f1b2c37656b409b0d64ca8..f3fa146562a1680be413e2ea10c92029df4db076 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/paul/theme/Colors.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/theme/Colors.kt
@@ -1,4 +1,4 @@
-package org.briarproject.briar.desktop.paul.theme
+package org.briarproject.briar.desktop.theme
 
 import androidx.compose.ui.graphics.Color
 
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/theme/Theme.kt b/src/main/kotlin/org/briarproject/briar/desktop/theme/Theme.kt
similarity index 97%
rename from src/main/kotlin/org/briarproject/briar/desktop/paul/theme/Theme.kt
rename to src/main/kotlin/org/briarproject/briar/desktop/theme/Theme.kt
index 48c517b85cdbc1cab7b0e01c33601ef49a32488b..d04411c20142f0cb2fdfa1fdfdbe6d732a8e4c62 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/paul/theme/Theme.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/theme/Theme.kt
@@ -1,4 +1,4 @@
-package org.briarproject.briar.desktop.paul.theme
+package org.briarproject.briar.desktop.theme
 
 import androidx.compose.foundation.isSystemInDarkTheme
 import androidx.compose.material.Colors
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/BriarUi.kt b/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt
similarity index 89%
rename from src/main/kotlin/org/briarproject/briar/desktop/BriarUi.kt
rename to src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt
index 52f9fa36d23b04765842638344bb7e4ade6d588a..e44791b91f7f92659bd76398f45a43e778543f6c 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/BriarUi.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/ui/BriarUi.kt
@@ -15,13 +15,13 @@ 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.dialogs.ContactsViewModel
-import org.briarproject.briar.desktop.dialogs.Login
-import org.briarproject.briar.desktop.dialogs.LoginViewModel
-import org.briarproject.briar.desktop.dialogs.Registration
-import org.briarproject.briar.desktop.dialogs.RegistrationViewModel
-import org.briarproject.briar.desktop.paul.theme.BriarTheme
-import org.briarproject.briar.desktop.paul.views.BriarUIStateManager
+import org.briarproject.briar.desktop.contact.ContactsViewModel
+import org.briarproject.briar.desktop.login.Login
+import org.briarproject.briar.desktop.login.LoginViewModel
+import org.briarproject.briar.desktop.login.Registration
+import org.briarproject.briar.desktop.login.RegistrationViewModel
+import org.briarproject.briar.desktop.theme.BriarTheme
+import org.briarproject.briar.desktop.ui.MainScreen
 import java.awt.Dimension
 import java.util.logging.Logger
 import javax.annotation.concurrent.Immutable
@@ -116,7 +116,7 @@ constructor(
                                 MM provides messagingManager,
                                 IM provides identityManager,
                             ) {
-                                BriarUIStateManager(contactsViewModel, isDark, setDark)
+                                MainScreen(contactsViewModel, isDark, setDark)
                             }
                     }
                 }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/ui/Constants.kt b/src/main/kotlin/org/briarproject/briar/desktop/ui/Constants.kt
new file mode 100644
index 0000000000000000000000000000000000000000..dc533cbe859a56cfdc0459a0bc26b28e8dde0f3c
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/ui/Constants.kt
@@ -0,0 +1,9 @@
+package org.briarproject.briar.desktop.ui
+
+import androidx.compose.ui.unit.dp
+
+object Constants {
+
+    val HEADER_SIZE = 56.dp
+    val CONTACTLIST_WIDTH = 275.dp
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/ui/HorizontalDivider.kt b/src/main/kotlin/org/briarproject/briar/desktop/ui/HorizontalDivider.kt
new file mode 100644
index 0000000000000000000000000000000000000000..eca9e54199977bc2b51a5f911c773d28375f3cab
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/ui/HorizontalDivider.kt
@@ -0,0 +1,14 @@
+package org.briarproject.briar.desktop.conversation
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material.Divider
+import androidx.compose.material.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import org.briarproject.briar.desktop.theme.divider
+
+@Composable
+fun HorizontalDivider(modifier: Modifier = Modifier) {
+    Divider(color = MaterialTheme.colors.divider, thickness = 1.dp, modifier = modifier.fillMaxWidth())
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/ui/Loader.kt b/src/main/kotlin/org/briarproject/briar/desktop/ui/Loader.kt
new file mode 100644
index 0000000000000000000000000000000000000000..95323440b85d61a0ebca97f48f17be5e0081a7ae
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/ui/Loader.kt
@@ -0,0 +1,20 @@
+package org.briarproject.briar.desktop.conversation
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.CircularProgressIndicator
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun Loader() {
+    Box(
+        contentAlignment = Alignment.Center,
+        modifier = Modifier.fillMaxWidth().padding(20.dp)
+    ) {
+        CircularProgressIndicator()
+    }
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/paul/views/BriarUIStateManager.kt b/src/main/kotlin/org/briarproject/briar/desktop/ui/MainScreen.kt
similarity index 50%
rename from src/main/kotlin/org/briarproject/briar/desktop/paul/views/BriarUIStateManager.kt
rename to src/main/kotlin/org/briarproject/briar/desktop/ui/MainScreen.kt
index d8699185fe69987793dbac0c4a821bb7c4babe4d..deb9801ddf592a0b488bc5959a81d9bcecd940c4 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/paul/views/BriarUIStateManager.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/ui/MainScreen.kt
@@ -1,10 +1,8 @@
-package org.briarproject.briar.desktop.paul.views
+package org.briarproject.briar.desktop.ui
 
 import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material.Button
 import androidx.compose.material.MaterialTheme
 import androidx.compose.material.Surface
 import androidx.compose.material.Text
@@ -12,18 +10,11 @@ import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
-import org.briarproject.bramble.api.contact.Contact
-import org.briarproject.briar.desktop.dialogs.ContactsViewModel
-
-enum class UiModes {
-    CONTACTS,
-    GROUPS,
-    FORUMS,
-    BLOGS,
-    TRANSPORTS,
-    SETTINGS,
-    SIGNOUT
-}
+import org.briarproject.briar.desktop.contact.ContactsViewModel
+import org.briarproject.briar.desktop.conversation.PrivateMessageView
+import org.briarproject.briar.desktop.conversation.VerticalDivider
+import org.briarproject.briar.desktop.settings.PlaceHolderSettingsView
+import org.briarproject.briar.desktop.views.BriarSidebar
 
 /*
  * This is the root of the tree, all state is held here and passed down to stateless composables, which render the UI
@@ -31,55 +22,30 @@ enum class UiModes {
  * Multiplatform, stateless, composable are found in briarCompose (possible briar-compose project in the future)
  */
 @Composable
-fun BriarUIStateManager(
+fun MainScreen(
     contactsViewModel: ContactsViewModel,
     isDark: Boolean,
     setDark: (Boolean) -> Unit
 ) {
     // current selected mode, changed using the sidebar buttons
-    val (uiMode, setUiMode) = remember { mutableStateOf(UiModes.CONTACTS) }
+    val (uiMode, setUiMode) = remember { mutableStateOf(UiMode.CONTACTS) }
     // TODO Figure out how to handle accounts with 0 contacts
     // current selected contact
-    val (contact, setContact) = remember { mutableStateOf(contact(contactsViewModel)) }
-    // current selected private group
-    val (group, setGroup) = remember { mutableStateOf(contact(contactsViewModel)) }
-    // current selected forum
-    val (forum, setForum) = remember { mutableStateOf(0) }
-    // current blog state
-    val (blog, setBlog) = remember { mutableStateOf(0) }
-    // current transport state
-    val (transport, setTransport) = remember { mutableStateOf(0) }
-    // current settings state
-    val (setting, setSetting) = remember { mutableStateOf(0) }
+    val (contact, setContact) = remember { mutableStateOf(contactsViewModel.getFirst()) }
     // Other global state that we need to track should go here also
     Row {
         BriarSidebar(uiMode, setUiMode)
         VerticalDivider()
         when (uiMode) {
-            UiModes.CONTACTS -> if (contact != null) PrivateMessageView(
+            UiMode.CONTACTS -> if (contact != null) PrivateMessageView(
                 contact,
                 contactsViewModel,
                 setContact
             )
-            UiModes.SETTINGS -> PlaceHolderSettingsView(isDark, setDark)
+            UiMode.SETTINGS -> PlaceHolderSettingsView(isDark, setDark)
             else -> Surface(modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.background)) {
                 Text("TBD")
             }
         }
     }
 }
-
-@Composable
-fun PlaceHolderSettingsView(isDark: Boolean, setDark: (Boolean) -> Unit) {
-    Surface(Modifier.fillMaxSize()) {
-        Box {
-            Button(onClick = { setDark(!isDark) }) {
-                Text("Change Theme")
-            }
-        }
-    }
-}
-
-fun contact(contacts: ContactsViewModel): Contact? {
-    return if (contacts.contacts.isEmpty()) null else contacts.contacts[0]
-}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/ui/UiMode.kt b/src/main/kotlin/org/briarproject/briar/desktop/ui/UiMode.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d30b74def9e28fe071329a2c966ddbdc48852960
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/ui/UiMode.kt
@@ -0,0 +1,11 @@
+package org.briarproject.briar.desktop.ui
+
+enum class UiMode {
+    CONTACTS,
+    GROUPS,
+    FORUMS,
+    BLOGS,
+    TRANSPORTS,
+    SETTINGS,
+    SIGNOUT
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/chat/UiState.kt b/src/main/kotlin/org/briarproject/briar/desktop/ui/UiState.kt
similarity index 81%
rename from src/main/kotlin/org/briarproject/briar/desktop/chat/UiState.kt
rename to src/main/kotlin/org/briarproject/briar/desktop/ui/UiState.kt
index 996727a4f146ebc8029d8b2ba8ce868642d03883..9c6d6a324e30c93a814c4f6c88f2a1adf626c803 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/chat/UiState.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/ui/UiState.kt
@@ -1,4 +1,4 @@
-package org.briarproject.briar.desktop.chat
+package org.briarproject.briar.desktop.ui
 
 sealed class UiState<out T> {
     object Loading : UiState<Nothing>()
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/ui/VerticalDivider.kt b/src/main/kotlin/org/briarproject/briar/desktop/ui/VerticalDivider.kt
new file mode 100644
index 0000000000000000000000000000000000000000..dc710e643412878a05e67b391448c0005ceea192
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/ui/VerticalDivider.kt
@@ -0,0 +1,15 @@
+package org.briarproject.briar.desktop.conversation
+
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.Divider
+import androidx.compose.material.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import org.briarproject.briar.desktop.theme.divider
+
+@Composable
+fun VerticalDivider() {
+    Divider(color = MaterialTheme.colors.divider, modifier = Modifier.fillMaxHeight().width(1.dp))
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/FileUtils.kt b/src/main/kotlin/org/briarproject/briar/desktop/utils/FileUtils.kt
similarity index 95%
rename from src/main/kotlin/org/briarproject/briar/desktop/FileUtils.kt
rename to src/main/kotlin/org/briarproject/briar/desktop/utils/FileUtils.kt
index f9aa4fa9126ee97cb0b0ffc733f3f34f3322d8a0..2556737721a12440518163fc4d651935f7cf7081 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/FileUtils.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/utils/FileUtils.kt
@@ -1,4 +1,4 @@
-package org.briarproject.briar.desktop
+package org.briarproject.briar.desktop.utils
 
 import org.briarproject.bramble.util.OsUtils
 import java.nio.file.Files
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/utils/TimeUtils.kt b/src/main/kotlin/org/briarproject/briar/desktop/utils/TimeUtils.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f59e52bd5f58df7513d11428a5e0476d5e58aaad
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/utils/TimeUtils.kt
@@ -0,0 +1,26 @@
+package org.briarproject.briar.desktop.utils
+
+import java.time.Instant
+import java.time.LocalDateTime
+import java.time.ZoneId
+import java.time.format.DateTimeFormatter
+import java.time.format.FormatStyle
+import java.time.temporal.ChronoUnit
+
+object TimeUtils {
+
+    fun getFormattedTimestamp(timestamp: Long): String {
+        val messageTime = LocalDateTime.ofInstant(
+            Instant.ofEpochMilli(timestamp), ZoneId.systemDefault()
+        )
+        val currentTime = LocalDateTime.now()
+        val difference = ChronoUnit.MINUTES.between(messageTime, currentTime)
+
+        val formatter = if (difference < 1440) { // = 1 day
+            DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
+        } else {
+            DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT)
+        }
+        return messageTime.format(formatter)
+    }
+}
diff --git a/src/test/kotlin/org/briarproject/briar/desktop/RunWithTemporaryAccount.kt b/src/test/kotlin/org/briarproject/briar/desktop/RunWithTemporaryAccount.kt
index e3375e8483d8f4f6e302b7cfc8c36f60a0dd0f49..681e3c9b9470ddfeb6655ec81441bf733905ba59 100644
--- a/src/test/kotlin/org/briarproject/briar/desktop/RunWithTemporaryAccount.kt
+++ b/src/test/kotlin/org/briarproject/briar/desktop/RunWithTemporaryAccount.kt
@@ -4,6 +4,7 @@ import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.window.application
 import org.briarproject.bramble.BrambleCoreEagerSingletons
 import org.briarproject.briar.BriarCoreEagerSingletons
+import org.briarproject.briar.desktop.utils.FileUtils
 import java.io.IOException
 import java.nio.file.Files
 import java.nio.file.Path