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