diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt
index 91bf6153a9ae5dd0c166e3ef2eb1515f5f555409..d0f935fc286aba751006bf9b5d1f11b125c89eb6 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactCard.kt
@@ -43,7 +43,6 @@ import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.semantics.contentDescription
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.text
-import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.style.TextOverflow.Companion.Ellipsis
 import androidx.compose.ui.unit.dp
 import org.briarproject.bramble.api.contact.ContactId
@@ -60,6 +59,7 @@ import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18nP
 import org.briarproject.briar.desktop.utils.PreviewUtils.preview
 import org.briarproject.briar.desktop.utils.TimeUtils.getFormattedTimestamp
 import org.briarproject.briar.desktop.utils.appendCommaSeparated
+import org.briarproject.briar.desktop.utils.buildBlankAnnotatedString
 import java.time.Instant
 
 @Suppress("HardCodedStringLiteral")
@@ -142,7 +142,7 @@ private fun RealContactRow(contactItem: ContactItem) {
         modifier = Modifier
             .fillMaxWidth()
             .semantics {
-                text = buildAnnotatedString {
+                text = buildBlankAnnotatedString {
                     append(i18nF("access.contact.with_name", contactItem.displayName))
                     appendCommaSeparated(
                         if (contactItem.isConnected) i18n("access.contact.connected.yes")
@@ -191,7 +191,7 @@ private fun PendingContactRow(contactItem: PendingContactItem, onRemove: () -> U
         modifier = Modifier
             .fillMaxWidth()
             .semantics {
-                text = buildAnnotatedString {
+                text = buildBlankAnnotatedString {
                     append(i18nF("access.contact.pending.with_name", contactItem.displayName))
                     // todo: include pending status
                     appendCommaSeparated(
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationItem.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationItem.kt
index fca3f6267a24761e0e49f8cf4ccb6ba5c1192010..b8ddaf904a9815558e5ae1e28e3c416bf5bacbb0 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationItem.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationItem.kt
@@ -29,6 +29,8 @@ sealed class ConversationItem {
     abstract val autoDeleteTimer: Long
     abstract val isIncoming: Boolean
 
+    inline val isOutgoing get() = !isIncoming
+
     /**
      * Only useful for incoming messages.
      */
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationItemView.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationItemView.kt
index c0bd2f9862a243ef49be2e5fdf8752132f91336e..fc66a399599536cb4ab406590186edbd55809f0d 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationItemView.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationItemView.kt
@@ -42,6 +42,8 @@ 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.semantics.semantics
+import androidx.compose.ui.semantics.text
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import org.briarproject.bramble.api.sync.GroupId
@@ -56,6 +58,8 @@ import org.briarproject.briar.desktop.theme.textPrimary
 import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
 import org.briarproject.briar.desktop.utils.PreviewUtils.preview
 import org.briarproject.briar.desktop.utils.TimeUtils.getFormattedTimestamp
+import org.briarproject.briar.desktop.utils.appendCommaSeparated
+import org.briarproject.briar.desktop.utils.buildBlankAnnotatedString
 import java.time.Instant
 
 @Suppress("HardCodedStringLiteral")
@@ -170,6 +174,7 @@ fun main() = preview {
 fun ConversationItemView(
     item: ConversationItem,
     onDelete: (MessageId) -> Unit = {},
+    conversationItemDescription: String,
     content: @Composable () -> Unit
 ) {
     val arrangement = if (item.isIncoming) Arrangement.Start else Arrangement.End
@@ -194,7 +199,18 @@ fun ConversationItemView(
                     elevation = 2.dp,
                     shape = shape,
                     border = BorderStroke(Dp.Hairline, MaterialTheme.colors.msgStroke),
-                    modifier = Modifier.padding(8.dp),
+                    modifier = Modifier.padding(8.dp)
+                        .semantics(mergeDescendants = true) {
+                            text = buildBlankAnnotatedString {
+                                append(getFormattedTimestamp(item.time))
+                                appendCommaSeparated(conversationItemDescription)
+                                if (item.isOutgoing) {
+                                    if (item.isSeen) appendCommaSeparated(i18n("access.conversation.status.seen"))
+                                    else if (item.isSent) appendCommaSeparated(i18n("access.conversation.status.sent"))
+                                    else appendCommaSeparated(i18n("access.conversation.status.scheduled"))
+                                }
+                            }
+                        },
                 ) {
                     content()
                 }
@@ -213,7 +229,7 @@ fun ColumnScope.ConversationItemStatusView(item: ConversationItem, rowModifier:
             style = MaterialTheme.typography.caption,
             color = statusColor,
         )
-        if (!item.isIncoming) {
+        if (item.isOutgoing) {
             val modifier = Modifier.padding(start = 4.dp).size(12.dp).align(Alignment.CenterVertically)
             val icon =
                 if (item.isSeen) Icons.Filled.DoneAll // acknowledged
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationList.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationList.kt
index cd7ff53c3802ee56d6cc6600f2b3434f71c1d641..bfc7c0406edddf628e9fadaf2a2caa790d05d341 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationList.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationList.kt
@@ -51,6 +51,10 @@ import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.text
+import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.unit.dp
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
@@ -183,7 +187,8 @@ fun ConversationList(
     Box(modifier = Modifier.padding(padding).fillMaxSize()) {
         LazyColumn(
             state = scrollState,
-            modifier = Modifier.fillMaxSize().padding(end = 8.dp),
+            modifier = Modifier.fillMaxSize().padding(end = 8.dp)
+                .semantics { contentDescription = i18n("access.conversation.list") },
         ) {
             itemsIndexed(messages) { idx, m ->
                 if (idx == initialFirstUnreadMessageIndex) {
@@ -274,6 +279,7 @@ fun UnreadMessagesMarker() = Box {
             .border(1.dp, MaterialTheme.colors.divider, RoundedCornerShape(16.dp))
             .background(MaterialTheme.colors.background)
             .padding(8.dp)
+            .semantics { text = AnnotatedString(i18n("access.conversation.message.unread")) }
     )
 }
 
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationMessageItemView.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationMessageItemView.kt
index 80d02700828784824301211f63ae89915e72b877..7e53a53d61a4a0b21b7977c0f76651ce7f028b1d 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationMessageItemView.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationMessageItemView.kt
@@ -32,7 +32,10 @@ import androidx.compose.ui.unit.dp
 import org.briarproject.bramble.api.sync.GroupId
 import org.briarproject.bramble.api.sync.MessageId
 import org.briarproject.briar.desktop.theme.textPrimary
+import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
+import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18nP
 import org.briarproject.briar.desktop.utils.PreviewUtils.preview
+import org.briarproject.briar.desktop.utils.appendAfterColon
 import java.time.Instant
 
 @Suppress("HardCodedStringLiteral")
@@ -68,7 +71,11 @@ fun ConversationMessageItemView(
     onDelete: (MessageId) -> Unit = {},
 ) {
     val textColor = if (m.isIncoming) MaterialTheme.colors.textPrimary else Color.White
-    ConversationItemView(m, onDelete) {
+    ConversationItemView(
+        item = m,
+        onDelete = onDelete,
+        conversationItemDescription = semanticDescription(m)
+    ) {
         Column(
             Modifier.padding(12.dp, 8.dp)
         ) {
@@ -91,3 +98,31 @@ fun ConversationMessageItemView(
         }
     }
 }
+
+private fun semanticDescription(m: ConversationMessageItem) = buildString {
+    if (m.isIncoming) {
+        if (m.attachments.isNotEmpty()) {
+            if (m.text == null)
+                append(i18nP("access.conversation.message.image.blank.your_contact", m.attachments.size))
+            else {
+                append(i18nP("access.conversation.message.image.caption.your_contact", m.attachments.size))
+                appendAfterColon(m.text)
+            }
+        } else {
+            append(i18n("access.conversation.message.blank.your_contact"))
+            appendAfterColon(m.text)
+        }
+    } else {
+        if (m.attachments.isNotEmpty()) {
+            if (m.text == null)
+                append(i18nP("access.conversation.message.image.blank.you", m.attachments.size))
+            else {
+                append(i18nP("access.conversation.message.image.caption.you", m.attachments.size))
+                appendAfterColon(m.text)
+            }
+        } else {
+            append(i18n("access.conversation.message.blank.you"))
+            appendAfterColon(m.text)
+        }
+    }
+}
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationNoticeItemView.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationNoticeItemView.kt
index d3eb5a4fe2232622e0515619d697c799e9ec3afc..d6a0aac97872f99016c01c40d2be0acef0a4738b 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationNoticeItemView.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationNoticeItemView.kt
@@ -41,7 +41,10 @@ import org.briarproject.briar.desktop.theme.noticeOut
 import org.briarproject.briar.desktop.theme.privateMessageDate
 import org.briarproject.briar.desktop.theme.textPrimary
 import org.briarproject.briar.desktop.theme.textSecondary
+import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
 import org.briarproject.briar.desktop.utils.PreviewUtils.preview
+import org.briarproject.briar.desktop.utils.appendAfterColon
+import org.briarproject.briar.desktop.utils.appendCommaSeparated
 import java.time.Instant
 
 @Suppress("HardCodedStringLiteral")
@@ -84,7 +87,17 @@ fun ConversationNoticeItemView(
     val noticeBackground = if (m.isIncoming) MaterialTheme.colors.noticeIn else MaterialTheme.colors.noticeOut
     val noticeColor = if (m.isIncoming) MaterialTheme.colors.textSecondary else MaterialTheme.colors.privateMessageDate
     val text = m.text
-    ConversationItemView(m, onDelete) {
+    ConversationItemView(
+        item = m,
+        onDelete = onDelete,
+        conversationItemDescription = buildString {
+            append(m.notice)
+            if (text != null) {
+                appendCommaSeparated(i18n("access.conversation.notice.additional_message"))
+                appendAfterColon(text)
+            }
+        }
+    ) {
         Column(Modifier.width(IntrinsicSize.Max)) {
             if (text != null) {
                 SelectionContainer {
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationRequestItemView.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationRequestItemView.kt
index ff66f9685c3cee9af1132ae486aca4baab4895c8..c61dadfd9d8746719cd94616236a0bc6a1d0a337 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationRequestItemView.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationRequestItemView.kt
@@ -50,6 +50,8 @@ import org.briarproject.briar.desktop.theme.textPrimary
 import org.briarproject.briar.desktop.theme.textSecondary
 import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n
 import org.briarproject.briar.desktop.utils.PreviewUtils.preview
+import org.briarproject.briar.desktop.utils.appendAfterColon
+import org.briarproject.briar.desktop.utils.appendCommaSeparated
 import java.time.Instant
 
 @Suppress("HardCodedStringLiteral")
@@ -98,9 +100,24 @@ fun ConversationRequestItemView(
     val textColor = if (m.isIncoming) MaterialTheme.colors.textPrimary else Color.White
     val noticeBackground = if (m.isIncoming) MaterialTheme.colors.noticeIn else MaterialTheme.colors.noticeOut
     val noticeColor = if (m.isIncoming) MaterialTheme.colors.textSecondary else MaterialTheme.colors.privateMessageDate
-    ConversationItemView(m, onDelete) {
+    val text = m.text
+    ConversationItemView(
+        item = m,
+        onDelete = onDelete,
+        conversationItemDescription = buildString {
+            append(m.notice)
+            if (text != null) {
+                appendCommaSeparated(i18n("access.conversation.notice.additional_message"))
+                appendAfterColon(text)
+            }
+            if (!m.answered) {
+                appendCommaSeparated(i18n("access.conversation.request.navigate_inside_to_react"))
+            } else if (m.canBeOpened) {
+                appendCommaSeparated(i18n("access.conversation.request.click_to_open"))
+            }
+        }
+    ) {
         Column(Modifier.width(IntrinsicSize.Max)) {
-            val text = m.text
             if (text != null) {
                 SelectionContainer {
                     Text(
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt
index 416e5a62450efdd423f61580a38f36497fa648bd..4f7a9b4a11cd8ab95782a7af49e73f4f01ede236 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt
@@ -404,7 +404,7 @@ constructor(
         seen: Boolean
     ) {
         val messages = HashSet(messageIds)
-        _messages.replaceIf({ !it.isIncoming && messages.contains(it.id) }) {
+        _messages.replaceIf({ it.isOutgoing && messages.contains(it.id) }) {
             it.mark(sent, seen)
         }
     }
diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/AnnotatedStringUtils.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/StringBuilderExt.kt
similarity index 54%
rename from briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/AnnotatedStringUtils.kt
rename to briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/StringBuilderExt.kt
index e1496c4ee7df07eb646286fa0a0b68b92df9e2e7..64712b7adfbbee1bae71270f280c469e40c8e9d1 100644
--- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/AnnotatedStringUtils.kt
+++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/StringBuilderExt.kt
@@ -19,11 +19,25 @@
 package org.briarproject.briar.desktop.utils
 
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.buildAnnotatedString
 
-fun AnnotatedString.Builder.appendLeading(leading: String, text: String? = null) {
+fun StringBuilder.appendLeading(leading: String, text: String? = null) {
     append(leading)
     if (text != null) append(text)
 }
 
-fun AnnotatedString.Builder.appendCommaSeparated(text: String? = null) =
+fun StringBuilder.appendCommaSeparated(text: String? = null) =
     appendLeading(", ", text)
+
+fun StringBuilder.appendAfterColon(text: String? = null) =
+    appendLeading(": ", text)
+
+/**
+ * Builds a new string by populating newly created [StringBuilder] using provided [builder]
+ * and then converting it to a blank [AnnotatedString] (without annotations).
+ *
+ * If a bare [String] is needed, use [buildString] instead.
+ * If an [AnnotatedString] with actual annotations is needed, use [buildAnnotatedString] instead.
+ */
+inline fun buildBlankAnnotatedString(builder: (StringBuilder).() -> Unit): AnnotatedString =
+    AnnotatedString(StringBuilder().apply(builder).toString())
diff --git a/briar-desktop/src/main/resources/strings/BriarDesktop.properties b/briar-desktop/src/main/resources/strings/BriarDesktop.properties
index c390a330870c18ad1b59e8a3204a9af1d4e3bca3..934b4a1be3a7d34b7883ff524f38f770d91014bf 100644
--- a/briar-desktop/src/main/resources/strings/BriarDesktop.properties
+++ b/briar-desktop/src/main/resources/strings/BriarDesktop.properties
@@ -15,6 +15,20 @@ access.contacts.dropdown.connections.expand=Expand connection menu
 access.contacts.dropdown.contacts.expand=Expand contact menu
 access.contacts.filter=Filter existing contacts by name or alias and add new contacts
 access.contacts.pending.remove=Remove pending contact
+access.conversation.list=Chat history
+access.conversation.status.seen=message received by your contact
+access.conversation.status.sent=message sent, but not received by your contact yet
+access.conversation.status.scheduled=message not sent yet
+access.conversation.message.unread=All messages below are still unread
+access.conversation.message.blank.you=you wrote
+access.conversation.message.blank.your_contact=your contact wrote
+access.conversation.message.image.blank.you={0, plural, one {you sent an image} other {you sent {0} images}}
+access.conversation.message.image.blank.your_contact={0, plural, one {your contact sent an image} other {your contact sent {0} images}}
+access.conversation.message.image.caption.you={0, plural, one {you sent an image and wrote} other {you sent {0} images and wrote}}
+access.conversation.message.image.caption.your_contact={0, plural, one {your contact sent an image and wrote} other {your contact sent {0} images and wrote}}
+access.conversation.notice.additional_message=additional message
+access.conversation.request.navigate_inside_to_react=navigate inside the item to react
+access.conversation.request.click_to_open=click to open
 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