diff --git a/src/main/kotlin/org/briarproject/briar/desktop/privategroups/PrivateGroupInfoDrawer.kt b/src/main/kotlin/org/briarproject/briar/desktop/privategroups/PrivateGroupInfoDrawer.kt
deleted file mode 100644
index ae32c89a81a0a8994801582ac9c6256675916eab..0000000000000000000000000000000000000000
--- a/src/main/kotlin/org/briarproject/briar/desktop/privategroups/PrivateGroupInfoDrawer.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package org.briarproject.briar.desktop.privategroups
-
-import androidx.compose.runtime.Composable
-import org.briarproject.briar.api.privategroup.PrivateGroup
-import org.briarproject.briar.desktop.contact.ContactInfoDrawerState
-
-// Right drawer state
-enum class PrivateGroupInfoDrawerState {
-    MakeIntro,
-    ConnectBT,
-    ConnectRD
-}
-
-@Composable
-fun ContactInfoDrawer(
-    privateGroup: PrivateGroup,
-    setInfoDrawer: (Boolean) -> Unit,
-    drawerState: ContactInfoDrawerState
-) {
-    /* TODO
-    when (drawerState) {
-        MakeIntro -> ContactDrawerMakeIntro(privateGroup, setInfoDrawer)
-    }
-     */
-}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/privategroups/PrivateGroupList.kt b/src/main/kotlin/org/briarproject/briar/desktop/privategroups/PrivateGroupList.kt
index 10304c6115c8fbb1d8dc44faf452459baa95ffc5..634a8671f46aa17edeb4fc8f306c7318ff213181 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/privategroups/PrivateGroupList.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/privategroups/PrivateGroupList.kt
@@ -1,9 +1,6 @@
 package org.briarproject.briar.desktop.privategroups
 
-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.lazy.LazyColumn
 import androidx.compose.foundation.lazy.items
@@ -11,16 +8,10 @@ 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.sync.GroupId
-import org.briarproject.briar.desktop.contact.SearchTextField
-import org.briarproject.briar.desktop.contact.add.remote.AddContactDialog
 import org.briarproject.briar.desktop.theme.surfaceVariant
-import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE
 import org.briarproject.briar.desktop.ui.Constants.PRIVATE_GROUP_LIST_WIDTH
 
 @Composable
@@ -28,25 +19,12 @@ fun PrivateGroupList(
     privateGroupList: List<PrivateGroupItem>,
     isSelected: (GroupId) -> Boolean,
     selectPrivateGroup: (GroupId) -> Unit,
-    filterBy: String,
-    setFilterBy: (String) -> Unit,
 ) {
-    var isCreatePrivateGroupDialogVisible by remember { mutableStateOf(false) }
-    if (isCreatePrivateGroupDialogVisible) AddContactDialog(onClose = { isCreatePrivateGroupDialogVisible = false })
+    // TODO AddPrivateGroupDialog
     Scaffold(
         modifier = Modifier.fillMaxHeight().width(PRIVATE_GROUP_LIST_WIDTH),
         backgroundColor = MaterialTheme.colors.surfaceVariant,
-        topBar = {
-            Column(
-                modifier = Modifier.fillMaxWidth().height(HEADER_SIZE + 1.dp),
-            ) {
-                SearchTextField(
-                    filterBy,
-                    onValueChange = setFilterBy,
-                    onContactAdd = { isCreatePrivateGroupDialogVisible = true }
-                )
-            }
-        },
+        // TODO SearchTextField
         content = {
             LazyColumn {
                 items(privateGroupList) { privateGroupItem ->
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/privategroups/PrivateGroupListViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/privategroups/PrivateGroupListViewModel.kt
index d3f27d8443d961fa24a61de0c8fece1bb835d71c..6e7c7de3e5c8a119ed357b1b2f63b621532e3264 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/privategroups/PrivateGroupListViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/privategroups/PrivateGroupListViewModel.kt
@@ -1,42 +1,31 @@
 package org.briarproject.briar.desktop.privategroups
 
 import androidx.compose.runtime.State
-import androidx.compose.runtime.mutableStateListOf
 import androidx.compose.runtime.mutableStateOf
 import org.briarproject.bramble.api.connection.ConnectionRegistry
-import org.briarproject.bramble.api.contact.ContactId
 import org.briarproject.bramble.api.event.Event
 import org.briarproject.bramble.api.event.EventBus
 import org.briarproject.bramble.api.sync.GroupId
 import org.briarproject.briar.api.conversation.ConversationManager
-import org.briarproject.briar.api.privategroup.PrivateGroup
 import org.briarproject.briar.api.privategroup.PrivateGroupManager
-import org.briarproject.briar.desktop.utils.removeFirst
-import org.briarproject.briar.desktop.utils.replaceFirst
 import org.briarproject.briar.desktop.viewmodel.BriarEventListenerViewModel
-import java.util.logging.Logger
 import javax.inject.Inject
 
 class PrivateGroupListViewModel
 @Inject
 constructor(
-    val privateGroupManager: PrivateGroupManager,
+    private val privateGroupManager: PrivateGroupManager,
     val conversationManager: ConversationManager,
     val connectionRegistry: ConnectionRegistry,
     eventBus: EventBus,
 ) : BriarEventListenerViewModel(eventBus) {
 
-    companion object {
-        private val LOG = Logger.getLogger(PrivateGroupListViewModel::class.java.name)
-    }
-
-    private val _fullContactList = mutableListOf<PrivateGroupItem>()
-    private val _filteredContactList = mutableStateListOf<PrivateGroupItem>()
+    private val _fullPrivateGroupList = mutableListOf<PrivateGroupItem>()
 
-    val privateGroupList: List<PrivateGroupItem> = _filteredContactList
+    val privateGroupList: List<PrivateGroupItem> = _fullPrivateGroupList
 
     private fun loadPrivateGroups() {
-        _fullContactList.apply {
+        _fullPrivateGroupList.apply {
             clear()
             addAll(
                 privateGroupManager.privateGroups.map { privateGroup ->
@@ -47,17 +36,6 @@ constructor(
                 }
             )
         }
-        updateFilteredList()
-    }
-
-    private fun updateItem(contactId: ContactId, update: (PrivateGroupItem) -> PrivateGroupItem) {
-        _fullContactList.replaceFirst({ it.privateGroup.id == contactId }, update)
-        updateFilteredList()
-    }
-
-    private fun removeItem(groupId: GroupId) {
-        _fullContactList.removeFirst { it.privateGroup.id == groupId }
-        updateFilteredList()
     }
 
     override fun onInit() {
@@ -65,10 +43,8 @@ constructor(
         loadPrivateGroups()
     }
 
-    private val _filterBy = mutableStateOf("")
     private val _selectedContactId = mutableStateOf<GroupId?>(null)
 
-    val filterBy: State<String> = _filterBy
     val selectedPrivateGroupId: State<GroupId?> = _selectedContactId
 
     fun selectPrivateGroup(privateGroupId: GroupId) {
@@ -77,55 +53,7 @@ constructor(
 
     fun isSelected(privateGroupId: GroupId) = _selectedContactId.value == privateGroupId
 
-    private fun filterContact(privateGroup: PrivateGroup) =
-        // todo: also filter on alias
-        privateGroup.name.contains(_filterBy.value, ignoreCase = true)
-
-    fun setFilterBy(filter: String) {
-        _filterBy.value = filter
-        updateFilteredList()
-    }
-
-    // todo: when migrated to StateFlow, this could be done implicitly instead
-    fun updateFilteredList() {
-        _filteredContactList.apply {
-            clear()
-            addAll(_fullContactList.filter { filterContact(it.privateGroup) }.sortedByDescending { it.timestamp })
-        }
-
-        // reset selected contact to null if not available after filtering
-        val id = _selectedContactId.value
-        if (id != null && !privateGroupList.map { it.privateGroup.id }.contains(id)) {
-            _selectedContactId.value = null
-        }
-    }
-
     override fun eventOccurred(e: Event?) {
-        /*
-        when (e) {
-            is ContactAddedEvent -> {
-                LOG.info("Contact added, reloading")
-                loadPrivateGroups()
-            }
-            is ContactRemovedEvent -> {
-                LOG.info("Contact removed, removing item")
-                removeItem(e.contactId)
-            }
-        }
-        when (e) {
-            is ConversationMessageReceivedEvent<*> -> {
-                LOG.info("Conversation message received, updating item")
-                updateItem(e.contactId) { it.updateFromMessageHeader(e.messageHeader) }
-            }
-            is ConversationMessageToBeSentEvent -> {
-                LOG.info("Conversation message added, updating item")
-                updateItem(e.contactId) { it.updateFromMessageHeader(e.messageHeader) }
-            }
-            // is AvatarUpdatedEvent -> {}
-            is ContactAliasChangedEvent -> {
-                updateItem(e.contactId) { it.updateAlias(e.alias) }
-            }
-        }
-         */
+        // TODO
     }
 }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/privategroups/PrivateGroupScreen.kt b/src/main/kotlin/org/briarproject/briar/desktop/privategroups/PrivateGroupScreen.kt
index 2046b4689db0c5ae9fc31d991d45aab6af81972b..7c3dac2f9e02a3ec8b2ce4e6fed530679e488227 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/privategroups/PrivateGroupScreen.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/privategroups/PrivateGroupScreen.kt
@@ -19,8 +19,6 @@ fun PrivateGroupScreen(
             viewModel.privateGroupList,
             viewModel::isSelected,
             viewModel::selectPrivateGroup,
-            viewModel.filterBy.value,
-            viewModel::setFilterBy
         )
         VerticalDivider()
         Column(modifier = Modifier.weight(1f).fillMaxHeight()) {
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/privategroups/ThreadedConversationHeader.kt b/src/main/kotlin/org/briarproject/briar/desktop/privategroups/ThreadedConversationHeader.kt
deleted file mode 100644
index 5146a684aa0f5fcbc86429e9a5526c39ea383d5c..0000000000000000000000000000000000000000
--- a/src/main/kotlin/org/briarproject/briar/desktop/privategroups/ThreadedConversationHeader.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-package org.briarproject.briar.desktop.privategroups
-
-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.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-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.briar.desktop.contact.ContactDropDown
-import org.briarproject.briar.desktop.contact.ProfileCircle
-import org.briarproject.briar.desktop.theme.outline
-import org.briarproject.briar.desktop.ui.Constants.HEADER_SIZE
-import org.briarproject.briar.desktop.ui.HorizontalDivider
-import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
-
-@Composable
-fun ThreadedConversationHeader(
-    privateGroupItem: PrivateGroupItem,
-    onMakeIntroduction: () -> Unit,
-) {
-    val (isExpanded, setExpanded) = remember { mutableStateOf(false) }
-    val outlineColor = MaterialTheme.colors.outline
-
-    Box(modifier = Modifier.fillMaxWidth().height(HEADER_SIZE + 1.dp)) {
-        Row(modifier = Modifier.align(Alignment.Center)) {
-            ProfileCircle(36.dp, privateGroupItem.privateGroup.id.bytes)
-            Text(
-                privateGroupItem.privateGroup.name,
-                modifier = Modifier.align(Alignment.CenterVertically).padding(start = 12.dp),
-                fontSize = 20.sp
-            )
-        }
-        IconButton(
-            onClick = { setExpanded(!isExpanded) },
-            modifier = Modifier.align(Alignment.CenterEnd).padding(end = 16.dp)
-        ) {
-            Icon(Icons.Filled.MoreVert, i18n("access.contact.menu"), modifier = Modifier.size(24.dp))
-            ContactDropDown(isExpanded, setExpanded, onMakeIntroduction)
-        }
-        HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter))
-    }
-}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/privategroups/ThreadedConversationItem.kt b/src/main/kotlin/org/briarproject/briar/desktop/privategroups/ThreadedConversationItem.kt
deleted file mode 100644
index 4ba592fd8867e9ebb646d30e5783c507d944fd73..0000000000000000000000000000000000000000
--- a/src/main/kotlin/org/briarproject/briar/desktop/privategroups/ThreadedConversationItem.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package org.briarproject.briar.desktop.privategroups
-
-import org.briarproject.bramble.api.sync.GroupId
-import org.briarproject.bramble.api.sync.MessageId
-
-sealed class ThreadedConversationItem {
-    abstract val id: MessageId
-    abstract val groupId: GroupId
-    abstract val time: Long
-    abstract val autoDeleteTimer: Long
-    abstract val isIncoming: Boolean
-
-    /**
-     * Only useful for incoming messages.
-     */
-    abstract val isRead: Boolean
-
-    /**
-     * Only useful for outgoing messages.
-     */
-    abstract val isSent: Boolean
-
-    /**
-     * Only useful for outgoing messages.
-     */
-    abstract val isSeen: Boolean
-
-    abstract fun mark(sent: Boolean, seen: Boolean): ThreadedConversationItem
-
-    abstract fun markRead(): ThreadedConversationItem
-}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/privategroups/ThreadedConversationMessageItem.kt b/src/main/kotlin/org/briarproject/briar/desktop/privategroups/ThreadedConversationMessageItem.kt
deleted file mode 100644
index f9b64bd3a4061aeeaef8912f117be08bdcb3acdd..0000000000000000000000000000000000000000
--- a/src/main/kotlin/org/briarproject/briar/desktop/privategroups/ThreadedConversationMessageItem.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package org.briarproject.briar.desktop.privategroups
-
-import org.briarproject.bramble.api.sync.GroupId
-import org.briarproject.bramble.api.sync.MessageId
-import org.briarproject.briar.api.conversation.ConversationMessageHeader
-
-data class ThreadedConversationMessageItem(
-    var text: String? = null,
-    override val id: MessageId,
-    override val groupId: GroupId,
-    override val time: Long,
-    override val autoDeleteTimer: Long,
-    override val isIncoming: Boolean,
-    override var isRead: Boolean,
-    override var isSent: Boolean,
-    override var isSeen: Boolean,
-
-    // todo: support attachments
-    // val attachments: List<AttachmentItem>
-) : ThreadedConversationItem() {
-
-    constructor(h: ConversationMessageHeader) :
-        this(
-            id = h.id,
-            groupId = h.groupId,
-            time = h.timestamp,
-            autoDeleteTimer = h.autoDeleteTimer,
-            isRead = h.isRead,
-            isSent = h.isSent,
-            isSeen = h.isSeen,
-            isIncoming = !h.isLocal,
-        )
-
-    override fun mark(sent: Boolean, seen: Boolean): ThreadedConversationItem {
-        return copy(isSent = sent, isSeen = seen)
-    }
-
-    override fun markRead(): ThreadedConversationItem {
-        return copy(isRead = true)
-    }
-}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/privategroups/ThreadedConversationScreen.kt b/src/main/kotlin/org/briarproject/briar/desktop/privategroups/ThreadedConversationScreen.kt
index db0b50ffc31f9f53eee2cc97b7d10beccdd50bc4..9dc2541a0aa18f8579795036be39ba955bde1ac2 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/privategroups/ThreadedConversationScreen.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/privategroups/ThreadedConversationScreen.kt
@@ -1,116 +1,12 @@
 package org.briarproject.briar.desktop.privategroups
 
-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.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.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.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-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.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.unit.dp
 import org.briarproject.bramble.api.sync.GroupId
-import org.briarproject.briar.desktop.contact.ContactInfoDrawer
-import org.briarproject.briar.desktop.contact.ContactInfoDrawerState
-import org.briarproject.briar.desktop.conversation.ConversationInput
-import org.briarproject.briar.desktop.navigation.SIDEBAR_WIDTH
-import org.briarproject.briar.desktop.theme.surfaceVariant
-import org.briarproject.briar.desktop.ui.Constants.CONTACTLIST_WIDTH
-import org.briarproject.briar.desktop.ui.Loader
+import org.briarproject.briar.desktop.ui.UiPlaceholder
 import org.briarproject.briar.desktop.viewmodel.viewModel
 
 @Composable
 fun ThreadedConversationScreen(
     groupId: GroupId,
     viewModel: ThreadedConversationViewModel = viewModel(),
-) {
-    LaunchedEffect(groupId) {
-        viewModel.setGroupId(groupId)
-    }
-
-    val contactItem = viewModel.contactItem.value
-
-    if (contactItem == null) {
-        Loader()
-        return
-    }
-
-    val (infoDrawer, setInfoDrawer) = remember { mutableStateOf(false) }
-    val (contactDrawerState, setDrawerState) = remember { mutableStateOf(ContactInfoDrawerState.MakeIntro) }
-    BoxWithConstraints(Modifier.fillMaxSize()) {
-        val animatedInfoDrawerOffsetX by animateDpAsState(if (infoDrawer) (-275).dp else 0.dp)
-        Scaffold(
-            topBar = {
-                ThreadedConversationHeader(
-                    contactItem,
-                    onMakeIntroduction = {
-                        setInfoDrawer(true)
-                    }
-                )
-            },
-            content = { padding ->
-                LazyColumn(
-                    verticalArrangement = Arrangement.spacedBy(8.dp),
-                    // reverseLayout to display most recent message (index 0) at the bottom
-                    reverseLayout = true,
-                    contentPadding = PaddingValues(8.dp),
-                    modifier = Modifier.padding(padding).fillMaxHeight()
-                ) {
-                    items(viewModel.messages) { m ->
-                        if (m is ThreadedConversationMessageItem)
-                            ThreadedText(m)
-                    }
-                }
-            },
-            bottomBar = {
-                ConversationInput(
-                    viewModel.newMessage.value,
-                    viewModel::setNewMessage,
-                    viewModel::sendMessage
-                )
-            },
-        )
-        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(contactItem.privateGroup, setInfoDrawer, contactDrawerState)
-            }
-        }
-    }
-}
+) = UiPlaceholder()
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/privategroups/ThreadedConversationViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/privategroups/ThreadedConversationViewModel.kt
index 05bc249a66785d6448ac61ebe24c8d442047fee6..fc5c31571a51e5e9a3eaa6603003c9b861beef4b 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/privategroups/ThreadedConversationViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/privategroups/ThreadedConversationViewModel.kt
@@ -1,33 +1,12 @@
 package org.briarproject.briar.desktop.privategroups
 
-import androidx.compose.runtime.State
-import androidx.compose.runtime.mutableStateListOf
-import androidx.compose.runtime.mutableStateOf
-import org.briarproject.bramble.api.FormatException
-import org.briarproject.bramble.api.contact.event.ContactRemovedEvent
-import org.briarproject.bramble.api.db.DbException
-import org.briarproject.bramble.api.db.NoSuchContactException
 import org.briarproject.bramble.api.event.Event
 import org.briarproject.bramble.api.event.EventBus
-import org.briarproject.bramble.api.sync.GroupId
-import org.briarproject.bramble.api.sync.MessageId
-import org.briarproject.bramble.api.sync.event.MessagesAckedEvent
-import org.briarproject.bramble.api.sync.event.MessagesSentEvent
-import org.briarproject.bramble.api.versioning.event.ClientVersionUpdatedEvent
-import org.briarproject.bramble.util.LogUtils
-import org.briarproject.briar.api.autodelete.UnexpectedTimerException
-import org.briarproject.briar.api.autodelete.event.ConversationMessagesDeletedEvent
 import org.briarproject.briar.api.conversation.ConversationManager
-import org.briarproject.briar.api.conversation.event.ConversationMessageReceivedEvent
 import org.briarproject.briar.api.messaging.MessagingManager
-import org.briarproject.briar.api.messaging.PrivateMessage
 import org.briarproject.briar.api.messaging.PrivateMessageFactory
-import org.briarproject.briar.api.messaging.PrivateMessageHeader
 import org.briarproject.briar.api.privategroup.PrivateGroupManager
-import org.briarproject.briar.desktop.utils.replaceIf
 import org.briarproject.briar.desktop.viewmodel.BriarEventListenerViewModel
-import java.util.Date
-import java.util.logging.Level
 import java.util.logging.Logger
 import javax.inject.Inject
 
@@ -45,188 +24,7 @@ constructor(
         private val LOG = Logger.getLogger(ThreadedConversationViewModel::class.java.name)
     }
 
-    private val _privateGroupId = mutableStateOf<GroupId?>(null)
-    private val _privateGroupItem = mutableStateOf<PrivateGroupItem?>(null)
-    private val _messages = mutableStateListOf<ThreadedConversationItem>()
-
-    private val _newMessage = mutableStateOf("")
-
-    val contactItem: State<PrivateGroupItem?> = _privateGroupItem
-    val messages: List<ThreadedConversationItem> = _messages
-
-    val newMessage: State<String> = _newMessage
-
-    fun setGroupId(id: GroupId) {
-        if (_privateGroupId.value == id)
-            return
-
-        _privateGroupId.value = id
-        _privateGroupItem.value = PrivateGroupItem(
-            privateGroupManager.getPrivateGroup(id),
-            privateGroupManager.getGroupCount(id),
-        )
-        loadMessages()
-        setNewMessage("")
-    }
-
-    fun setNewMessage(msg: String) {
-        _newMessage.value = msg
-    }
-
-    fun sendMessage() {
-        try {
-            val text = _newMessage.value
-            _newMessage.value = ""
-
-            // don't send empty or blank messages
-            if (text.isBlank()) return
-
-            val start = LogUtils.now()
-            val m = createMessage(text)
-            messagingManager.addLocalMessage(m)
-            LogUtils.logDuration(LOG, "Storing message", start)
-
-            val message = m.message
-            val h = PrivateMessageHeader(
-                message.id, message.groupId,
-                message.timestamp, true, true, false, false,
-                m.hasText(), m.attachmentHeaders,
-                m.autoDeleteTimer
-            )
-            _messages.add(0, messageHeaderToItem(h))
-            // eventBus.broadcast(ConversationMessageToBeSentEvent(h, _contactId.value!!))
-        } catch (e: UnexpectedTimerException) {
-            LogUtils.logException(LOG, Level.WARNING, e)
-        } catch (e: DbException) {
-            LogUtils.logException(LOG, Level.WARNING, e)
-        }
-    }
-
-    @Throws(DbException::class)
-    private fun createMessage(text: String): PrivateMessage {
-        val groupId = _privateGroupItem.value!!.privateGroup.id
-        // todo: this API call needs a database transaction context
-        // val timestamp = conversationManager.getTimestampForOutgoingMessage(_contactId.value!!)
-        val timestamp = Date().time
-        try {
-            return privateMessageFactory.createLegacyPrivateMessage(
-                groupId, timestamp, text
-            )
-        } catch (e: FormatException) {
-            throw AssertionError(e)
-        }
-    }
-
-    private fun loadMessages() {
-        try {
-            val start = LogUtils.now()
-            val headers = privateGroupManager.getHeaders(_privateGroupId.value!!)
-            LogUtils.logDuration(LOG, "Loading message headers", start)
-            // Sort headers by timestamp in *descending* order
-            val sorted = headers.sortedByDescending { it.timestamp }
-            _messages.apply {
-                clear()
-                val start = LogUtils.now()
-                addAll(
-                    // todo: use ConversationVisitor to also display Request and Notice Messages
-                    sorted.filterIsInstance<PrivateMessageHeader>().map(::messageHeaderToItem)
-                )
-                LogUtils.logDuration(LOG, "Loading messages", start)
-            }
-        } catch (e: NoSuchContactException) {
-            LogUtils.logException(LOG, Level.WARNING, e)
-        } catch (e: DbException) {
-            LogUtils.logException(LOG, Level.WARNING, e)
-        }
-    }
-
-    private fun messageHeaderToItem(h: PrivateMessageHeader): ThreadedConversationMessageItem {
-        // todo: use ConversationVisitor instead and support other MessageHeader
-        val item = ThreadedConversationMessageItem(h)
-        if (h.hasText()) {
-            item.text = loadMessageText(h.id)
-        } else {
-            LOG.warning { "private message without text" }
-        }
-        return item
-    }
-
-    private fun loadMessageText(m: MessageId): String? {
-        try {
-            return messagingManager.getMessageText(m)
-        } catch (e: DbException) {
-            LogUtils.logException(LOG, Level.WARNING, e)
-        }
-        return null
-    }
-
     override fun eventOccurred(e: Event?) {
-        when (e) {
-            is ContactRemovedEvent -> {
-                if (e.contactId == _privateGroupId.value) {
-                    LOG.info("Contact removed")
-                    // todo: we probably don't need to react to this here as the ContactsViewModel should already handle it
-                }
-            }
-            is ConversationMessageReceivedEvent<*> -> {
-                if (e.contactId == _privateGroupId.value) {
-                    LOG.info("Message received, adding")
-                    val h = e.messageHeader
-                    if (h is PrivateMessageHeader) {
-                        // insert at start of list according to descending sort order
-                        _messages.add(0, messageHeaderToItem(h))
-                    }
-                }
-            }
-            is MessagesSentEvent -> {
-                if (e.contactId == _privateGroupId.value) {
-                    LOG.info("Messages sent")
-                    markMessages(e.messageIds, sent = true, seen = false)
-                }
-            }
-            is MessagesAckedEvent -> {
-                if (e.contactId == _privateGroupId.value) {
-                    LOG.info("Messages acked")
-                    markMessages(e.messageIds, sent = true, seen = true)
-                }
-            }
-            is ConversationMessagesDeletedEvent -> {
-                if (e.contactId == _privateGroupId.value) {
-                    LOG.info("Messages auto-deleted")
-                    val messages = HashSet(e.messageIds)
-                    _messages.removeIf { messages.contains(it.id) }
-                }
-            }
-            /*
-            is ContactConnectedEvent -> {
-                if (e.contactId == _privateGroupId.value) {
-                    LOG.info("Contact connected")
-                    _privateGroupItem.value = _privateGroupItem.value!!.updateIsConnected(true)
-                }
-            }
-            is ContactDisconnectedEvent -> {
-                if (e.contactId == _privateGroupId.value) {
-                    LOG.info("Contact disconnected")
-                    _privateGroupItem.value = _privateGroupItem.value!!.updateIsConnected(false)
-                }
-            }
-             */
-            is ClientVersionUpdatedEvent -> {
-                if (e.contactId == _privateGroupId.value) {
-                    // todo: still not implemented
-                }
-            }
-        }
-    }
-
-    private fun markMessages(
-        messageIds: Collection<MessageId>,
-        sent: Boolean,
-        seen: Boolean
-    ) {
-        val messages = HashSet(messageIds)
-        _messages.replaceIf({ !it.isIncoming && messages.contains(it.id) }) {
-            it.mark(sent, seen)
-        }
+        // TODO
     }
 }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/privategroups/ThreadedText.kt b/src/main/kotlin/org/briarproject/briar/desktop/privategroups/ThreadedText.kt
deleted file mode 100644
index 4b5eabad729c1f2dcaabae2ab265ee8870a5dc94..0000000000000000000000000000000000000000
--- a/src/main/kotlin/org/briarproject/briar/desktop/privategroups/ThreadedText.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-package org.briarproject.briar.desktop.privategroups
-
-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.Done
-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.unit.dp
-import androidx.compose.ui.unit.sp
-import org.briarproject.briar.desktop.theme.awayMsgBubble
-import org.briarproject.briar.desktop.theme.localMsgBubble
-import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
-import org.briarproject.briar.desktop.utils.TimeUtils
-
-@Composable
-fun ThreadedText(m: ThreadedConversationMessageItem) {
-    val alignment = if (m.isIncoming) Alignment.Start else Alignment.End
-    val color = if (m.isIncoming) MaterialTheme.colors.awayMsgBubble else MaterialTheme.colors.localMsgBubble
-    val shape = if (m.isIncoming)
-        RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp, bottomEnd = 10.dp)
-    else
-        RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp, bottomStart = 10.dp)
-
-    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.text!!, fontSize = 14.sp, modifier = Modifier.align(Alignment.Start))
-                    Row(modifier = Modifier.padding(top = 4.dp)) {
-                        Text(TimeUtils.getFormattedTimestamp(m.time), Modifier.padding(end = 4.dp), fontSize = 10.sp)
-                        if (!m.isIncoming) {
-                            val modifier = Modifier.size(12.dp).align(Alignment.CenterVertically)
-                            val icon =
-                                if (m.isSeen) Icons.Filled.DoneAll // acknowledged
-                                else if (m.isSent) Icons.Filled.Done // sent
-                                else Icons.Filled.Schedule // waiting
-                            Icon(icon, i18n("access.message.sent"), modifier)
-                        }
-                    }
-                }
-            }
-        }
-    }
-}