diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItem.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItem.kt index 62ebc584ecb602dde7bd4478d65345c8fc6f51a5..2ac68bc97de1277ad9e5176b913330398b2cb6a3 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItem.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactItem.kt @@ -45,4 +45,7 @@ data class ContactItem( fun updateAlias(a: String?): ContactItem { return copy(alias = a) } + + fun updateFromMessagesRead(c: Int): ContactItem = + copy(unread = unread - c) } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt index 254ce8fe4ed9f48d0c2fb13f3db319de203b41e1..cf26aea936405ab5111b443068d6462939197ceb 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/contact/ContactListViewModel.kt @@ -11,6 +11,7 @@ import org.briarproject.bramble.api.event.Event import org.briarproject.bramble.api.event.EventBus import org.briarproject.briar.api.conversation.ConversationManager import org.briarproject.briar.api.conversation.event.ConversationMessageTrackedEvent +import org.briarproject.briar.desktop.conversation.ConversationMessagesReadEvent import javax.inject.Inject class ContactListViewModel @@ -72,6 +73,10 @@ constructor( is ContactAliasChangedEvent -> { updateItem(e.contactId) { it.updateAlias(e.alias) } } + is ConversationMessagesReadEvent -> { + LOG.info("${e.count} conversation messages read, updating item") + updateItem(e.contactId) { it.updateFromMessagesRead(e.count) } + } } } } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationMessagesReadEvent.kt b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationMessagesReadEvent.kt new file mode 100644 index 0000000000000000000000000000000000000000..659067a0c401cc41b83809fbfef640c4b32f01ff --- /dev/null +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationMessagesReadEvent.kt @@ -0,0 +1,13 @@ +package org.briarproject.briar.desktop.conversation + +import org.briarproject.bramble.api.contact.ContactId +import org.briarproject.bramble.api.event.Event + +/** + * An event that is broadcast when conversation messages + * are shown on the screen for the first time. + */ +data class ConversationMessagesReadEvent( + val count: Int, + val contactId: ContactId +) : Event() 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 8390eb90a048f227da1e3020f47c7d32036d24d8..0b3d1898ba2b9dda9929667a4fc7b245ac1cfe73 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationScreen.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationScreen.kt @@ -17,6 +17,7 @@ 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.MaterialTheme import androidx.compose.material.Scaffold @@ -55,6 +56,8 @@ fun ConversationScreen( val (infoDrawer, setInfoDrawer) = remember { mutableStateOf(false) } val (contactDrawerState, setDrawerState) = remember { mutableStateOf(ContactInfoDrawerState.MakeIntro) } + val scrollState = rememberLazyListState() + BoxWithConstraints(Modifier.fillMaxSize()) { val animatedInfoDrawerOffsetX by animateDpAsState(if (infoDrawer) (-275).dp else 0.dp) Scaffold( @@ -69,6 +72,7 @@ fun ConversationScreen( content = { padding -> LazyColumn( verticalArrangement = Arrangement.spacedBy(8.dp), + state = scrollState, // reverseLayout to display most recent message (index 0) at the bottom reverseLayout = true, contentPadding = PaddingValues(8.dp), @@ -88,6 +92,14 @@ 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 affc255f9a9a4a2b55502f5ac0cbd898d9f687db..79e76b2c13219fd237d0c5af753091aa839a2908 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationViewModel.kt @@ -1,6 +1,7 @@ package org.briarproject.briar.desktop.conversation import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import mu.KotlinLogging @@ -31,6 +32,7 @@ import org.briarproject.briar.api.messaging.PrivateMessageHeader import org.briarproject.briar.desktop.contact.ContactItem import org.briarproject.briar.desktop.utils.KLoggerUtils.logDuration import org.briarproject.briar.desktop.utils.replaceIf +import org.briarproject.briar.desktop.utils.replaceIfIndexed import org.briarproject.briar.desktop.viewmodel.BriarEventListenerViewModel import java.util.Date import javax.inject.Inject @@ -108,6 +110,18 @@ constructor( } } + val hasUnreadMessages = derivedStateOf { _messages.any { !it.isRead } } + + fun markMessagesRead(untilIndex: Int) { + var count = 0 + _messages.replaceIfIndexed({ idx, it -> idx >= untilIndex && !it.isRead }) { _, it -> + conversationManager.setReadFlag(it.groupId, it.id, true) + count++ + it.markRead() + } + eventBus.broadcast(ConversationMessagesReadEvent(count, contactItem.value!!.contactId)) + } + @Throws(DbException::class) private fun createMessage(text: String): PrivateMessage { val groupId = messagingManager.getConversationId(_contactItem.value!!.contactId) diff --git a/src/main/kotlin/org/briarproject/briar/desktop/utils/ListUtils.kt b/src/main/kotlin/org/briarproject/briar/desktop/utils/ListUtils.kt index b2221c8a5275f3e4e375fb438c4677fc04111744..5f4b3cd60976c37d7bb95ecfcf8bef46ce72024e 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/utils/ListUtils.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/utils/ListUtils.kt @@ -10,6 +10,18 @@ fun <T> MutableList<T>.replaceIf(predicate: (T) -> Boolean, transformation: (T) } } +fun <T> MutableList<T>.replaceIfIndexed(predicate: (Int, T) -> Boolean, transformation: (Int, T) -> T) { + val li = listIterator() + var index = 0 + while (li.hasNext()) { + val n = li.next() + if (predicate(index, n)) { + li.set(transformation(index, n)) + } + index++ + } +} + fun <T> MutableList<T>.replaceFirst(predicate: (T) -> Boolean, transformation: (T) -> T) { val li = listIterator() while (li.hasNext()) {