From 79f539a27b7c5dc4242bc0adde1ef44f8720d563 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Sun, 23 Mar 2025 08:45:01 +0300 Subject: [PATCH] - read indicator, edit status and time for message in messages history --- .../fast/model/api/domain/VkConversation.kt | 35 ++++++ .../meloda/fast/model/api/domain/VkMessage.kt | 11 +- .../MessagesHistoryViewModel.kt | 100 ++++++++++++------ .../model/MessagesHistoryScreenState.kt | 7 +- .../fast/messageshistory/model/UiItem.kt | 4 +- .../presentation/IncomingMessageBubble.kt | 15 +-- .../presentation/MessageBubble.kt | 88 ++++++++++----- .../presentation/MessagesList.kt | 2 + .../presentation/OutgoingMessageBubble.kt | 18 +--- .../messageshistory/util/MessageMapper.kt | 7 +- 10 files changed, 192 insertions(+), 95 deletions(-) diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkConversation.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkConversation.kt index 5eb47897..c54a6ad7 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkConversation.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkConversation.kt @@ -38,6 +38,41 @@ data class VkConversation( fun isPinned(): Boolean = majorId > 0 fun isInUnread() = inRead - (lastMessageId ?: 0) < 0 fun isOutUnread() = outRead - (lastMessageId ?: 0) < 0 + + companion object { + val EMPTY: VkConversation = VkConversation( + id = -1, + localId = -1, + ownerId = null, + title = "...", + photo50 = null, + photo100 = null, + photo200 = null, + isCallInProgress = false, + isPhantom = false, + lastConversationMessageId = -1, + inReadCmId = -1, + outReadCmId = -1, + inRead = -1, + outRead = -1, + lastMessageId = null, + unreadCount = -1, + membersCount = null, + canChangePin = false, + canChangeInfo = false, + majorId = -1, + minorId = -1, + pinnedMessageId = null, + interactionType = -1, + interactionIds = emptyList(), + peerType = PeerType.USER, + lastMessage = null, + pinnedMessage = null, + user = null, + group = null + + ) + } } fun VkConversation.asEntity(): VkConversationEntity = VkConversationEntity( diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkMessage.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkMessage.kt index 9e86f436..6829b0ce 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkMessage.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkMessage.kt @@ -38,12 +38,11 @@ data class VkMessage( fun isGroup() = fromId < 0 - fun isRead(conversation: VkConversation) = - if (isOut) { - conversation.outRead - id >= 0 - } else { - conversation.inRead - id >= 0 - } + fun isRead(conversation: VkConversation): Boolean = when { + id <= 0 -> false + isOut -> conversation.outRead - id >= 0 + else -> conversation.inRead - id >= 0 + } fun hasAttachments(): Boolean = attachments.orEmpty().isNotEmpty() diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt index 24596d07..9f910401 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt @@ -177,22 +177,22 @@ class MessagesHistoryViewModelImpl( val newMessage = message.asPresentation( resourceProvider = resourceProvider, - showDate = false, showName = false, prevMessage = prevMessage, nextMessage = null, - showTimeInActionMessages = userSettings.showTimeInActionMessages.value + showTimeInActionMessages = userSettings.showTimeInActionMessages.value, + conversation = screenState.value.conversation, ) newMessages.add(0, newMessage) prevMessage?.let { prev -> newMessages[1] = prev.asPresentation( resourceProvider = resourceProvider, - showDate = false, showName = false, prevMessage = prevMessage, nextMessage = messages.value.first(), - showTimeInActionMessages = userSettings.showTimeInActionMessages.value + showTimeInActionMessages = userSettings.showTimeInActionMessages.value, + conversation = screenState.value.conversation ) } @@ -208,11 +208,11 @@ class MessagesHistoryViewModelImpl( ?.let { index -> val newMessage = message.asPresentation( resourceProvider = resourceProvider, - showDate = false, showName = false, prevMessage = messages.value.getOrNull(index + 1), nextMessage = messages.value.getOrNull(index - 1), - showTimeInActionMessages = userSettings.showTimeInActionMessages.value + showTimeInActionMessages = userSettings.showTimeInActionMessages.value, + conversation = screenState.value.conversation ) val newMessages = screenState.value.messages.toMutableList() @@ -227,7 +227,37 @@ class MessagesHistoryViewModelImpl( } private fun handleReadOutgoingEvent(event: LongPollEvent.VkMessageReadOutgoingEvent) { + if (event.peerId != screenState.value.conversationId) return + val messages = messages.value + val messageIndex = + messages.indexOfFirstOrNull { it.id == event.messageId } + + if (messageIndex == null) { // диалога нет в списке + // pizdets + } else { + val newConversation = screenState.value.conversation.copy( + outRead = event.messageId + ) + + val uiMessages = messages.mapIndexed { index, item -> + item.asPresentation( + resourceProvider = resourceProvider, + showName = false, + prevMessage = messages.getOrNull(index + 1), + nextMessage = messages.getOrNull(index - 1), + showTimeInActionMessages = userSettings.showTimeInActionMessages.value, + conversation = newConversation + ) + } + + screenState.setValue { old -> + old.copy( + conversation = newConversation, + messages = uiMessages, + ) + } + } } private fun loadMessagesHistory(offset: Int = currentOffset.value) { @@ -239,9 +269,7 @@ class MessagesHistoryViewModelImpl( offset = offset, ).listenValue(viewModelScope) { state -> state.processState( - error = { error -> - - }, + error = { error -> }, success = { response -> val messages = response.messages val fullMessages = if (offset == 0) { @@ -259,16 +287,6 @@ class MessagesHistoryViewModelImpl( messagesUseCase.storeMessages(messages) conversationsUseCase.storeConversations(conversations) - val loadedMessages = fullMessages.mapIndexed { index, message -> - message.asPresentation( - resourceProvider = resourceProvider, - showDate = false, - showName = false, - prevMessage = messages.getOrNull(index + 1), - nextMessage = messages.getOrNull(index - 1), - showTimeInActionMessages = userSettings.showTimeInActionMessages.value - ) - } val itemsCountSufficient = messages.size == MESSAGES_LOAD_COUNT @@ -281,15 +299,28 @@ class MessagesHistoryViewModelImpl( conversations .firstOrNull { it.id == screenState.value.conversationId } ?.let { conversation -> + screenState.setValue { old -> old.copy(conversation = conversation) } newState = newState.copy( title = conversation.extractTitle( useContactName = AppSettings.General.useContactNames, resources = resourceProvider.resources ), - avatar = conversation.extractAvatar() + avatar = conversation.extractAvatar(), + conversation = conversation ) } + val loadedMessages = fullMessages.mapIndexed { index, message -> + message.asPresentation( + resourceProvider = resourceProvider, + showName = false, + prevMessage = messages.getOrNull(index + 1), + nextMessage = messages.getOrNull(index - 1), + showTimeInActionMessages = userSettings.showTimeInActionMessages.value, + conversation = screenState.value.conversation + ) + } + this.messages.emit(fullMessages) screenState.setValue { newState.copy(messages = loadedMessages) } canPaginate.setValue { itemsCountSufficient } @@ -350,18 +381,14 @@ class MessagesHistoryViewModelImpl( val newMessages = screenState.value.messages.toMutableList() val newUiMessage = newMessage.asPresentation( resourceProvider = resourceProvider, - showDate = false, showName = false, prevMessage = messages.value.firstOrNull(), nextMessage = null, - showTimeInActionMessages = userSettings.showTimeInActionMessages.value + showTimeInActionMessages = userSettings.showTimeInActionMessages.value, + conversation = screenState.value.conversation ) newMessages.add(0, newUiMessage) - messages.setValue { old -> - listOf(newMessage).plus(old) - } - screenState.setValue { old -> old.copy( message = TextFieldValue(), @@ -382,17 +409,22 @@ class MessagesHistoryViewModelImpl( sendingMessages -= newMessage }, success = { messageId -> - sendingMessages += newMessage + sendingMessages -= newMessage - val messages = screenState.value.messages.toMutableList() + val uiMessages = screenState.value.messages.toMutableList() + messages.setValue { old -> + listOf(newMessage.copy(id = messageId)).plus(old) + } - messages.indexOfOrNull(newUiMessage)?.let { index -> - (messages[index] as? UiItem.Message)?.let { message -> - messages[index] = message.copy(id = messageId) + uiMessages.indexOfOrNull(newUiMessage)?.let { index -> + (uiMessages[index] as? UiItem.Message)?.let { message -> + uiMessages[index] = message + .copy(id = messageId) + .copy(isRead = newMessage.isRead(screenState.value.conversation)) } } - screenState.setValue { old -> old.copy(messages = messages) } + screenState.setValue { old -> old.copy(messages = uiMessages) } } ) } @@ -511,11 +543,11 @@ class MessagesHistoryViewModelImpl( val uiMessages = messages.mapIndexed { index, item -> item.asPresentation( resourceProvider = resourceProvider, - showDate = false, showName = false, prevMessage = messages.getOrNull(index + 1), nextMessage = messages.getOrNull(index - 1), - showTimeInActionMessages = show + showTimeInActionMessages = show, + conversation = screenState.value.conversation ) } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/MessagesHistoryScreenState.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/MessagesHistoryScreenState.kt index a8312f0a..815e2d56 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/MessagesHistoryScreenState.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/MessagesHistoryScreenState.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable import androidx.compose.ui.text.input.TextFieldValue import dev.meloda.fast.common.model.UiImage import dev.meloda.fast.model.api.domain.VkAttachment +import dev.meloda.fast.model.api.domain.VkConversation @Immutable data class MessagesHistoryScreenState( @@ -18,7 +19,8 @@ data class MessagesHistoryScreenState( val isPaginating: Boolean, val isPaginationExhausted: Boolean, val actionMode: ActionMode, - val chatImageUrl: String? + val chatImageUrl: String?, + val conversation: VkConversation ) { companion object { @@ -34,7 +36,8 @@ data class MessagesHistoryScreenState( isPaginating = false, isPaginationExhausted = false, actionMode = ActionMode.Record, - chatImageUrl = null + chatImageUrl = null, + conversation = VkConversation.EMPTY ) } } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/UiItem.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/UiItem.kt index f2a98738..fbfbffb7 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/UiItem.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/UiItem.kt @@ -22,7 +22,8 @@ sealed class UiItem( val showAvatar: Boolean, val showName: Boolean, val avatar: UiImage, - val isEdited: Boolean + val isEdited: Boolean, + val isRead: Boolean ) : UiItem(id, conversationMessageId) data class ActionMessage( @@ -32,4 +33,3 @@ sealed class UiItem( val actionCmId: Int? ) : UiItem(id, conversationMessageId) } - diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/IncomingMessageBubble.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/IncomingMessageBubble.kt index a000ae57..bb13bc7a 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/IncomingMessageBubble.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/IncomingMessageBubble.kt @@ -31,6 +31,7 @@ import dev.meloda.fast.messageshistory.model.UiItem fun IncomingMessageBubble( modifier: Modifier = Modifier, message: UiItem.Message, + animate: Boolean ) { val context = LocalContext.current @@ -44,12 +45,12 @@ fun IncomingMessageBubble( if (message.isInChat) { Image( painter = - message.avatar.extractUrl()?.let { url -> - rememberAsyncImagePainter( - model = url, - imageLoader = context.imageLoader - ) - } ?: painterResource(id = message.avatar.extractResId()), + message.avatar.extractUrl()?.let { url -> + rememberAsyncImagePainter( + model = url, + imageLoader = context.imageLoader + ) + } ?: painterResource(id = message.avatar.extractResId()), contentDescription = null, modifier = Modifier .padding(bottom = 6.dp) @@ -80,6 +81,8 @@ fun IncomingMessageBubble( isOut = false, date = message.date, edited = message.isEdited, + animate = animate, + isRead = message.isRead ) } } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessageBubble.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessageBubble.kt index 9f20c622..abb7d6e2 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessageBubble.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessageBubble.kt @@ -1,19 +1,32 @@ package dev.meloda.fast.messageshistory.presentation import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Create +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp +import dev.meloda.fast.ui.R as UiR @Composable fun MessageBubble( @@ -22,6 +35,8 @@ fun MessageBubble( isOut: Boolean, date: String?, edited: Boolean, + animate: Boolean, + isRead: Boolean ) { val backgroundColor = if (!isOut) { MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) @@ -45,44 +60,61 @@ fun MessageBubble( vertical = 6.dp ) ) { + val minDateContainerWidth = remember(edited, isOut) { + val mainPart = if (edited) 50.dp else 30.dp + val readIndicatorPart = if (isOut) 14.dp else 0.dp + + mainPart + readIndicatorPart + } + + val dateContainerWidth by animateDpAsState( + targetValue = minDateContainerWidth, + label = "dateContainerWidth" + ) + if (text != null) { Text( text = text, modifier = Modifier .padding(2.dp) .align(Alignment.Center) - .animateContentSize(), + .padding(end = 4.dp) + .padding(end = dateContainerWidth) + .padding(end = 4.dp) + .then(if (animate) Modifier.animateContentSize() else Modifier), color = textColor ) } + Row( + modifier = Modifier + .align(Alignment.BottomEnd) + .defaultMinSize(minWidth = dateContainerWidth) + ) { + if (edited) { + Icon( + imageVector = Icons.Rounded.Create, + contentDescription = null, + modifier = Modifier.size(14.dp) + ) + Spacer(modifier = Modifier.width(4.dp)) + } + Text( + text = date.orEmpty(), + style = MaterialTheme.typography.labelSmall, + ) + Spacer(modifier = Modifier.width(4.dp)) -// val dateContainerWidth by animateDpAsState( -// targetValue = if (edited) 50.dp else 30.dp, -// label = "dateContainerWidth" -// ) - -// AnimatedVisibility( -// date != null, -// modifier = Modifier -// .width(dateContainerWidth) -// .align(Alignment.BottomEnd) -// ) { -// Row(modifier = Modifier.fillMaxWidth()) { -// if (edited) { -// Icon( -// imageVector = Icons.Rounded.Create, -// contentDescription = null, -// modifier = Modifier.size(14.dp) -// ) -// Spacer(modifier = Modifier.width(4.dp)) -// } -// Text( -// text = date.orEmpty(), -// style = MaterialTheme.typography.labelSmall -// ) -// Spacer(modifier = Modifier.width(2.dp)) -// } -// } + if (isOut) { + Icon( + modifier = Modifier.size(14.dp), + painter = painterResource( + if (isRead) UiR.drawable.round_done_all_24 + else UiR.drawable.ic_round_done_24 + ), + contentDescription = null + ) + } + } } } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt index d0f7a539..ad9b7c0e 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt @@ -98,6 +98,7 @@ fun MessagesList( else Modifier ), message = item, + animate = enableAnimations ) } else { IncomingMessageBubble( @@ -110,6 +111,7 @@ fun MessagesList( else Modifier ), message = item, + animate = enableAnimations ) } } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/OutgoingMessageBubble.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/OutgoingMessageBubble.kt index dc1ad033..4ccade7c 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/OutgoingMessageBubble.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/OutgoingMessageBubble.kt @@ -3,12 +3,8 @@ package dev.meloda.fast.messageshistory.presentation import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -20,6 +16,7 @@ import dev.meloda.fast.messageshistory.model.UiItem fun OutgoingMessageBubble( modifier: Modifier = Modifier, message: UiItem.Message, + animate: Boolean ) { Row( modifier = modifier.fillMaxWidth(), @@ -37,18 +34,11 @@ fun OutgoingMessageBubble( modifier = Modifier, text = message.text.orDots(), isOut = true, - date = null, + date = message.date, edited = message.isEdited, + animate = animate, + isRead = message.isRead ) - - if (message.showDate) { - Spacer(modifier = Modifier.height(2.dp)) - Text( - modifier = Modifier.padding(end = 12.dp), - text = message.date, - style = MaterialTheme.typography.labelSmall - ) - } } } } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/util/MessageMapper.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/util/MessageMapper.kt index 233fc3dc..715082e2 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/util/MessageMapper.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/util/MessageMapper.kt @@ -90,8 +90,8 @@ fun VkConversation.extractTitle( }.parseString(resources).orDots() fun VkMessage.asPresentation( + conversation: VkConversation, resourceProvider: ResourceProvider, - showDate: Boolean, showName: Boolean, prevMessage: VkMessage?, nextMessage: VkMessage?, @@ -118,11 +118,12 @@ fun VkMessage.asPresentation( randomId = randomId, isInChat = isPeerChat(), name = extractTitle(), - showDate = showDate, + showDate = true, showAvatar = extractShowAvatar(nextMessage), showName = showName && extractShowName(prevMessage), avatar = extractAvatar(), - isEdited = updateTime != null + isEdited = updateTime != null, + isRead = isRead(conversation) ) }