diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt
index 8b6744ddd4ff5e5e21e83852f9e5d1edcbba9d39..b3bce311e4aa1512c3d75115b1b5b5736054a54b 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt
@@ -50,7 +50,7 @@ fun main() = preview(
     ContactCard(
         ContactItem(
             idWrapper = RealContactIdWrapper(ContactId(0)),
-            authorId = AuthorId(getRandomId()),
+            authorId = AuthorId(getRandomIdPersistent()),
             name = getStringParameter("name"),
             alias = getStringParameter("alias"),
             isConnected = getBooleanParameter("isConnected"),
@@ -88,7 +88,7 @@ fun ContactCard(
                             ProfileCircle(36.dp, contactItem)
                             MessageCounter(
                                 unread = contactItem.unread,
-                                modifier = Modifier.align(Alignment.TopEnd)
+                                modifier = Modifier.align(Alignment.TopEnd).offset(6.dp, (-6).dp)
                             )
                         }
                         RealContactInfo(
@@ -130,7 +130,6 @@ fun MessageCounter(unread: Int, modifier: Modifier = Modifier) {
     if (unread > 0) {
         Box(
             modifier = modifier
-                .offset(6.dp, (-6).dp)
                 .height(20.dp)
                 .widthIn(min = 20.dp, max = Dp.Infinity)
                 .border(2.dp, outlineColor, CircleShape)
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationItemView.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationItemView.kt
index 6dbbd900320400dfe3a9a97f0a026e54201c5c90..09c0e01c93a34878f30941c132ad98414346154f 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationItemView.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationItemView.kt
@@ -49,8 +49,8 @@ fun main() = preview {
                 ConversationNoticeItem(
                     notice = "Text of notice message.",
                     text = "Let's test a received notice message.",
-                    id = MessageId(getRandomId()),
-                    groupId = GroupId(getRandomId()),
+                    id = MessageId(getRandomIdPersistent()),
+                    groupId = GroupId(getRandomIdPersistent()),
                     time = Instant.now().toEpochMilli(),
                     autoDeleteTimer = 0,
                     isIncoming = true,
@@ -64,8 +64,8 @@ fun main() = preview {
             ConversationMessageItemView(
                 ConversationMessageItem(
                     text = "This is a medium-sized message that has been sent before receiving the request message.",
-                    id = MessageId(getRandomId()),
-                    groupId = GroupId(getRandomId()),
+                    id = MessageId(getRandomIdPersistent()),
+                    groupId = GroupId(getRandomIdPersistent()),
                     time = Instant.now().toEpochMilli(),
                     autoDeleteTimer = 0,
                     isIncoming = false,
@@ -80,12 +80,12 @@ fun main() = preview {
                 ConversationRequestItem(
                     requestedGroupId = null,
                     requestType = INTRODUCTION,
-                    sessionId = SessionId(getRandomId()),
+                    sessionId = SessionId(getRandomIdPersistent()),
                     answered = false,
                     notice = "Text of notice message.",
                     text = "Short message.",
-                    id = MessageId(getRandomId()),
-                    groupId = GroupId(getRandomId()),
+                    id = MessageId(getRandomIdPersistent()),
+                    groupId = GroupId(getRandomIdPersistent()),
                     time = Instant.now().toEpochMilli(),
                     autoDeleteTimer = 0,
                     isIncoming = true,
@@ -100,8 +100,8 @@ fun main() = preview {
                 ConversationNoticeItem(
                     notice = "Text of notice message.",
                     text = "This is a long long long message that spans over several lines.\n\nIt ends here.",
-                    id = MessageId(getRandomId()),
-                    groupId = GroupId(getRandomId()),
+                    id = MessageId(getRandomIdPersistent()),
+                    groupId = GroupId(getRandomIdPersistent()),
                     time = Instant.now().toEpochMilli(),
                     autoDeleteTimer = 0,
                     isIncoming = false,
@@ -115,8 +115,8 @@ fun main() = preview {
             ConversationMessageItemView(
                 ConversationMessageItem(
                     text = "Just also receiving a normal message.",
-                    id = MessageId(getRandomId()),
-                    groupId = GroupId(getRandomId()),
+                    id = MessageId(getRandomIdPersistent()),
+                    groupId = GroupId(getRandomIdPersistent()),
                     time = Instant.now().toEpochMilli(),
                     autoDeleteTimer = 0,
                     isIncoming = true,
@@ -131,8 +131,8 @@ fun main() = preview {
                 ConversationNoticeItem(
                     notice = "Text of notice message.",
                     text = null,
-                    id = MessageId(getRandomId()),
-                    groupId = GroupId(getRandomId()),
+                    id = MessageId(getRandomIdPersistent()),
+                    groupId = GroupId(getRandomIdPersistent()),
                     time = Instant.now().toEpochMilli(),
                     autoDeleteTimer = 0,
                     isIncoming = false,
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationList.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationList.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ddc487fe0bcad4614588440dd74a5f410b3ece1e
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationList.kt
@@ -0,0 +1,213 @@
+package org.briarproject.briar.desktop.conversation
+
+import androidx.compose.foundation.VerticalScrollbar
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+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.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.rememberScrollbarAdapter
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.FloatingActionButton
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.produceState
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import org.briarproject.bramble.api.sync.GroupId
+import org.briarproject.bramble.api.sync.MessageId
+import org.briarproject.briar.desktop.contact.MessageCounter
+import org.briarproject.briar.desktop.theme.ChevronDown
+import org.briarproject.briar.desktop.theme.ChevronUp
+import org.briarproject.briar.desktop.theme.divider
+import org.briarproject.briar.desktop.ui.HorizontalDivider
+import org.briarproject.briar.desktop.ui.Loader
+import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
+import org.briarproject.briar.desktop.utils.PreviewUtils.preview
+import org.briarproject.briar.desktop.utils.replaceIfIndexed
+import java.time.Instant
+
+fun main() = preview(
+    "num_messages" to 20,
+    "first_unread_index" to 5
+) {
+    val numMessages = getIntParameter("num_messages")
+    val initialFirstUnreadIndex = getIntParameter("first_unread_index")
+
+    // re-create messages and currentUnreadMessageInfo as soon as numMessages or initialFirstUnreadIndex change
+    val loading by produceState(true, numMessages, initialFirstUnreadIndex) { value = true; delay(500); value = false }
+
+    val messages = remember(loading) {
+        mutableStateListOf<ConversationItem>().apply {
+            addAll(
+                (0 until numMessages).map { idx ->
+                    ConversationMessageItem(
+                        text = "Example Text $idx",
+                        id = MessageId(getRandomId()),
+                        groupId = GroupId(getRandomId()),
+                        time = Instant.now().minusSeconds((numMessages - idx).toLong() * 60).toEpochMilli(),
+                        autoDeleteTimer = 0,
+                        isIncoming = idx % 2 == 0,
+                        isRead = idx % 2 == 1 || idx < initialFirstUnreadIndex,
+                        isSent = false,
+                        isSeen = false
+                    )
+                }
+            )
+        }
+    }
+
+    val currentUnreadMessagesInfo by remember(loading) {
+        derivedStateOf {
+            ConversationViewModel.UnreadMessagesInfo(
+                amount = messages.count { !it.isRead },
+                firstIndex = messages.indexOfFirst { !it.isRead }
+            )
+        }
+    }
+
+    if (loading) {
+        Loader()
+        return@preview
+    }
+
+    ConversationList(
+        padding = PaddingValues(0.dp),
+        messages = messages,
+        initialFirstUnreadMessageIndex = initialFirstUnreadIndex,
+        currentUnreadMessagesInfo = currentUnreadMessagesInfo,
+        markMessagesRead = { lst ->
+            messages.replaceIfIndexed(
+                { idx, it -> idx in lst && !it.isRead },
+                { _, it -> it.markRead() }
+            )
+        },
+        respondToRequest = { _, _ -> },
+        deleteMessage = {}
+    )
+}
+
+@Composable
+fun ConversationList(
+    padding: PaddingValues,
+    messages: List<ConversationItem>,
+    initialFirstUnreadMessageIndex: Int,
+    currentUnreadMessagesInfo: ConversationViewModel.UnreadMessagesInfo,
+    markMessagesRead: (List<Int>) -> Unit,
+    respondToRequest: (ConversationRequestItem, Boolean) -> Unit,
+    deleteMessage: (MessageId) -> Unit,
+) {
+    // we need to make sure the ConversationList is out of composition before showing new messages
+    // so that the list state and the coroutine scope is created anew
+    // this is currently assured by (briefly) showing a loader while loading the messages
+    val scope = rememberCoroutineScope()
+    val scrollState = rememberLazyListState(
+        if (initialFirstUnreadMessageIndex != -1) initialFirstUnreadMessageIndex
+        else if (messages.isNotEmpty()) messages.lastIndex
+        else 0
+    )
+
+    Box(modifier = Modifier.padding(padding).fillMaxSize()) {
+        LazyColumn(
+            state = scrollState,
+            contentPadding = PaddingValues(top = 8.dp, bottom = 8.dp),
+            modifier = Modifier.fillMaxSize().padding(end = 12.dp)
+        ) {
+            itemsIndexed(messages) { idx, m ->
+                if (idx == initialFirstUnreadMessageIndex) {
+                    UnreadMessagesMarker()
+                }
+                when (m) {
+                    is ConversationMessageItem -> ConversationMessageItemView(m)
+                    is ConversationNoticeItem -> ConversationNoticeItemView(m)
+                    is ConversationRequestItem ->
+                        ConversationRequestItemView(
+                            m,
+                            onResponse = { accept -> respondToRequest(m, accept) },
+                        )
+                }
+            }
+        }
+        VerticalScrollbar(
+            adapter = rememberScrollbarAdapter(scrollState),
+            modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight()
+        )
+
+        if (currentUnreadMessagesInfo.amount > 0) {
+            val delayUntilMarkedAsRead = 500L
+            LaunchedEffect(currentUnreadMessagesInfo, scrollState.firstVisibleItemIndex) {
+                // mark all messages visible on the screen for more than [delayUntilMarkedAsRead] milliseconds as read
+                delay(delayUntilMarkedAsRead)
+                markMessagesRead(scrollState.layoutInfo.visibleItemsInfo.map { it.index })
+            }
+            val showUnreadButton by produceState(false) {
+                // never show FAB before all messages currently visible are marked as read
+                delay(delayUntilMarkedAsRead + 100)
+                value = true
+            }
+
+            if (showUnreadButton) {
+                UnreadMessagesFAB(
+                    arrowUp = currentUnreadMessagesInfo.firstIndex < scrollState.firstVisibleItemIndex,
+                    counter = currentUnreadMessagesInfo.amount,
+                    onClick = {
+                        scope.launch {
+                            scrollState.animateScrollToItem(currentUnreadMessagesInfo.firstIndex, 0)
+                        }
+                    },
+                    modifier = Modifier.align(Alignment.BottomEnd).padding(32.dp) // todo: check padding
+                )
+            }
+        }
+    }
+}
+
+@Composable
+fun UnreadMessagesMarker() = Box {
+    HorizontalDivider(Modifier.align(Alignment.Center))
+    Text(
+        text = i18n("conversation.message.unread"),
+        modifier = Modifier
+            .align(Alignment.Center)
+            .padding(8.dp)
+            .border(1.dp, MaterialTheme.colors.divider, RoundedCornerShape(16.dp))
+            .background(MaterialTheme.colors.background)
+            .padding(8.dp)
+    )
+}
+
+@Composable
+fun UnreadMessagesFAB(
+    arrowUp: Boolean,
+    counter: Int,
+    onClick: () -> Unit,
+    modifier: Modifier = Modifier,
+) = Box(modifier) {
+    FloatingActionButton(onClick) {
+        val arrow = if (arrowUp)
+            Icons.Filled.ChevronUp else Icons.Filled.ChevronDown
+        Icon(arrow, i18n("access.message.jump_to_unread"))
+    }
+    MessageCounter(
+        counter,
+        Modifier.align(Alignment.TopEnd).offset(3.dp, (-3).dp)
+    )
+}
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationMessageItemView.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationMessageItemView.kt
index 3619b174251fa5b64689355d0c0f85303463b015..43ba815cea392e61f038454e72e595651e903cb8 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationMessageItemView.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationMessageItemView.kt
@@ -27,8 +27,8 @@ fun main() = preview(
     ConversationMessageItemView(
         ConversationMessageItem(
             text = getStringParameter("text"),
-            id = MessageId(getRandomId()),
-            groupId = GroupId(getRandomId()),
+            id = MessageId(getRandomIdPersistent()),
+            groupId = GroupId(getRandomIdPersistent()),
             time = getLongParameter("time"),
             autoDeleteTimer = 0,
             isIncoming = getBooleanParameter("isIncoming"),
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationNoticeItemView.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationNoticeItemView.kt
index 8a79f0b1f96accd13c7ca7b0efcb85c90e675d56..733c6e5bd3c7840180c2855640adb956d66821cb 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationNoticeItemView.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationNoticeItemView.kt
@@ -38,8 +38,8 @@ fun main() = preview(
         ConversationNoticeItem(
             notice = getStringParameter("notice"),
             text = getStringParameter("text"),
-            id = MessageId(getRandomId()),
-            groupId = GroupId(getRandomId()),
+            id = MessageId(getRandomIdPersistent()),
+            groupId = GroupId(getRandomIdPersistent()),
             time = getLongParameter("time"),
             autoDeleteTimer = 0,
             isIncoming = getBooleanParameter("isIncoming"),
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationRequestItemView.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationRequestItemView.kt
index 865919e7a91b209c176339df9d9f97279f4f50ee..73db6fc889c5a218c18600a3f95f06c92ba028e3 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationRequestItemView.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationRequestItemView.kt
@@ -47,14 +47,14 @@ fun main() = preview(
 ) {
     ConversationRequestItemView(
         ConversationRequestItem(
-            requestedGroupId = if (getBooleanParameter("canBeOpened")) GroupId(getRandomId()) else null,
+            requestedGroupId = if (getBooleanParameter("canBeOpened")) GroupId(getRandomIdPersistent()) else null,
             requestType = INTRODUCTION,
-            sessionId = SessionId(getRandomId()),
+            sessionId = SessionId(getRandomIdPersistent()),
             answered = getBooleanParameter("answered"),
             notice = getStringParameter("notice"),
             text = getStringParameter("text"),
-            id = MessageId(getRandomId()),
-            groupId = GroupId(getRandomId()),
+            id = MessageId(getRandomIdPersistent()),
+            groupId = GroupId(getRandomIdPersistent()),
             time = getLongParameter("time"),
             autoDeleteTimer = 0,
             isIncoming = getBooleanParameter("isIncoming"),
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationScreen.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationScreen.kt
index 89b4a307476aadb896897cace783f53945087827..f7eff442c0f8830cbae81417243a7ac24bff9597 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationScreen.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationScreen.kt
@@ -7,16 +7,11 @@ 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.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.lazy.rememberLazyListState
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.ExperimentalMaterialApi
 import androidx.compose.material.MaterialTheme
@@ -58,7 +53,6 @@ fun ConversationScreen(
     val (infoDrawer, setInfoDrawer) = remember { mutableStateOf(false) }
     val (contactDrawerState, setDrawerState) = remember { mutableStateOf(ContactInfoDrawerState.MakeIntro) }
     val (deleteAllMessagesDialogVisible, setDeleteAllMessagesDialog) = remember { mutableStateOf(false) }
-    val scrollState = rememberLazyListState()
 
     BoxWithConstraints(Modifier.fillMaxSize()) {
         val animatedInfoDrawerOffsetX by animateDpAsState(if (infoDrawer) (-275).dp else 0.dp)
@@ -79,27 +73,15 @@ fun ConversationScreen(
                     Loader()
                     return@Scaffold
                 }
-
-                LazyColumn(
-                    state = scrollState,
-                    // 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 ->
-                        when (m) {
-                            is ConversationMessageItem -> ConversationMessageItemView(m, viewModel::deleteMessage)
-                            is ConversationNoticeItem -> ConversationNoticeItemView(m, viewModel::deleteMessage)
-                            is ConversationRequestItem ->
-                                ConversationRequestItemView(
-                                    m,
-                                    onResponse = { accept -> viewModel.respondToRequest(m, accept) },
-                                    onDelete = viewModel::deleteMessage,
-                                )
-                        }
-                    }
-                }
+                ConversationList(
+                    padding,
+                    viewModel.messages,
+                    viewModel.initialFirstUnreadMessageIndex.value,
+                    viewModel.currentUnreadMessagesInfo.value,
+                    viewModel::markMessagesRead,
+                    viewModel::respondToRequest,
+                    viewModel::deleteMessage,
+                )
             },
             bottomBar = {
                 ConversationInput(
@@ -110,13 +92,6 @@ fun ConversationScreen(
             },
         )
 
-        if (viewModel.hasUnreadMessages.value) {
-            LaunchedEffect(scrollState.firstVisibleItemIndex) {
-                // mark all messages older than the first visible item as read
-                viewModel.markMessagesRead(scrollState.firstVisibleItemIndex)
-            }
-        }
-
         if (infoDrawer) {
             // TODO Find non-hacky way of setting scrim on entire app
             Box(
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt
index 85da3d34147320b7a3d1cd65e8b3c736212362ac..969423ab5a62c6fcba468ca55aa675fa1d73c3af 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt
@@ -133,7 +133,7 @@ constructor(
                 )
                 val visitor = ConversationVisitor(contactItem.value!!.name, messagingManager, txn)
                 val msg = h.accept(visitor)!!
-                txn.attach { _messages.add(0, msg) }
+                txn.attach { _messages.add(msg) }
             } catch (e: UnexpectedTimerException) {
                 // todo: handle this properly
                 LOG.warn(e) {}
@@ -144,19 +144,35 @@ constructor(
         }
     }
 
-    val hasUnreadMessages = derivedStateOf { _messages.any { !it.isRead } }
+    // first unread message when first opening the list
+    // used to draw a horizontal divider on that position as long as list is opened
+    // we cannot use [derivedStateOf] here as it would move the line after first showing the list
+    private val _initialFirstUnreadMessageIndex = mutableStateOf(-1)
+    val initialFirstUnreadMessageIndex = _initialFirstUnreadMessageIndex.asState()
+
+    val currentUnreadMessagesInfo = derivedStateOf {
+        UnreadMessagesInfo(
+            amount = _messages.count { !it.isRead },
+            firstIndex = _messages.indexOfFirst { !it.isRead }
+        )
+    }
+
+    data class UnreadMessagesInfo(
+        val amount: Int,
+        val firstIndex: Int
+    )
 
-    fun markMessagesRead(untilIndex: Int) {
+    fun markMessagesRead(indices: List<Int>) {
         val id = _contactId.value!!
         val messages = _messages.toList()
         runOnDbThreadWithTransaction(false) { txn ->
             var count = 0
-            messages.filterIndexed { idx, it -> idx >= untilIndex && !it.isRead }.forEach {
+            messages.filterIndexed { idx, it -> idx in indices && !it.isRead }.forEach {
                 conversationManager.setReadFlag(txn, it.groupId, it.id, true)
                 count++
             }
             txn.attach {
-                _messages.replaceIfIndexed({ idx, it -> idx >= untilIndex && !it.isRead }) { _, it ->
+                _messages.replaceIfIndexed({ idx, it -> idx in indices && !it.isRead }) { _, it ->
                     it.markRead()
                 }
             }
@@ -209,12 +225,16 @@ constructor(
             val headers = conversationManager.getMessageHeaders(txn, contact.idWrapper.contactId)
             LOG.logDuration("Loading message headers", start)
             // Sort headers by timestamp in *descending* order
-            val sorted = headers.sortedByDescending { it.timestamp }
+            // val sorted = headers.sortedByDescending { it.timestamp }
+            val sorted = headers.sortedBy { it.timestamp }
             start = LogUtils.now()
             val visitor = ConversationVisitor(contact.name, messagingManager, txn)
             val messages = sorted.map { h -> h.accept(visitor)!! }
             LOG.logDuration("Loading messages", start)
-            txn.attach { _messages.clearAndAddAll(messages) }
+            txn.attach {
+                _messages.clearAndAddAll(messages)
+                _initialFirstUnreadMessageIndex.value = messages.indexOfFirst { !it.isRead }
+            }
         } catch (e: NoSuchContactException) {
             // todo: handle this properly
             LOG.warn(e) {}
@@ -239,7 +259,7 @@ constructor(
                     runOnDbThreadWithTransaction(true) { txn ->
                         val visitor = ConversationVisitor(contactItem.value!!.name, messagingManager, txn)
                         val msg = h.accept(visitor)!!
-                        txn.attach { _messages.add(0, msg) }
+                        txn.attach { _messages.add(msg) }
                     }
                 }
             }
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/theme/Icons.kt b/src/main/kotlin/org/briarproject/briar/desktop/theme/Icons.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0ca7efe177905dc3823be101e7f4ddee91af7aff
--- /dev/null
+++ b/src/main/kotlin/org/briarproject/briar/desktop/theme/Icons.kt
@@ -0,0 +1,8 @@
+package org.briarproject.briar.desktop.theme
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ExpandLess
+import androidx.compose.material.icons.filled.ExpandMore
+
+val Icons.Filled.ChevronUp get() = ExpandLess
+val Icons.Filled.ChevronDown get() = ExpandMore
diff --git a/src/main/kotlin/org/briarproject/briar/desktop/utils/PreviewUtils.kt b/src/main/kotlin/org/briarproject/briar/desktop/utils/PreviewUtils.kt
index 2af666641ee99b47d5684ad439af973ad828128e..967b0e4bb606c32e9f123642e1f3ef23abc84569 100644
--- a/src/main/kotlin/org/briarproject/briar/desktop/utils/PreviewUtils.kt
+++ b/src/main/kotlin/org/briarproject/briar/desktop/utils/PreviewUtils.kt
@@ -64,9 +64,11 @@ object PreviewUtils {
 
         fun setLongParameter(name: String, value: Long) = setDatatype(name, value)
 
+        fun getRandomId() = random.nextBytes(UniqueId.LENGTH)
+
         @Composable
-        fun getRandomId() =
-            remember { random.nextBytes(UniqueId.LENGTH) }
+        fun getRandomIdPersistent() =
+            remember { getRandomId() }
     }
 
     @Composable
diff --git a/src/main/resources/strings/BriarDesktop.properties b/src/main/resources/strings/BriarDesktop.properties
index 038922edf28accb623d93fa47885caad00ec8616..26999bf9f8ca9b1aa78ba393bf61495b494dff4b 100644
--- a/src/main/resources/strings/BriarDesktop.properties
+++ b/src/main/resources/strings/BriarDesktop.properties
@@ -7,6 +7,7 @@ access.contacts.dropdown.contacts.expand=Expand contacts menu
 access.contacts.search=Icon for searching contacts
 access.introduction.back.contact=Go back to contact screen of introduction process
 access.introduction.close=Close introduction screen
+access.message.jump_to_unread=Jump to next unread message
 access.message.send=Send message
 access.message.sent=Message sent
 access.logo=Briar logo
@@ -30,6 +31,7 @@ contacts.dropdown.introduction=Make Introduction
 contacts.search.title=Contacts
 
 # Conversation
+conversation.message.unread=Unread messages
 conversation.message.new=New Message
 conversation.delete.all.dialog.title=Confirm Message Deletion
 conversation.delete.all.dialog.message=Are you sure that you want to delete all messages?