- read indicator, edit status and time for message in messages history

This commit is contained in:
2025-03-23 08:45:01 +03:00
parent 4cc6ec6b5d
commit 79f539a27b
10 changed files with 192 additions and 95 deletions
@@ -38,6 +38,41 @@ data class VkConversation(
fun isPinned(): Boolean = majorId > 0 fun isPinned(): Boolean = majorId > 0
fun isInUnread() = inRead - (lastMessageId ?: 0) < 0 fun isInUnread() = inRead - (lastMessageId ?: 0) < 0
fun isOutUnread() = outRead - (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( fun VkConversation.asEntity(): VkConversationEntity = VkConversationEntity(
@@ -38,11 +38,10 @@ data class VkMessage(
fun isGroup() = fromId < 0 fun isGroup() = fromId < 0
fun isRead(conversation: VkConversation) = fun isRead(conversation: VkConversation): Boolean = when {
if (isOut) { id <= 0 -> false
conversation.outRead - id >= 0 isOut -> conversation.outRead - id >= 0
} else { else -> conversation.inRead - id >= 0
conversation.inRead - id >= 0
} }
fun hasAttachments(): Boolean = attachments.orEmpty().isNotEmpty() fun hasAttachments(): Boolean = attachments.orEmpty().isNotEmpty()
@@ -177,22 +177,22 @@ class MessagesHistoryViewModelImpl(
val newMessage = message.asPresentation( val newMessage = message.asPresentation(
resourceProvider = resourceProvider, resourceProvider = resourceProvider,
showDate = false,
showName = false, showName = false,
prevMessage = prevMessage, prevMessage = prevMessage,
nextMessage = null, nextMessage = null,
showTimeInActionMessages = userSettings.showTimeInActionMessages.value showTimeInActionMessages = userSettings.showTimeInActionMessages.value,
conversation = screenState.value.conversation,
) )
newMessages.add(0, newMessage) newMessages.add(0, newMessage)
prevMessage?.let { prev -> prevMessage?.let { prev ->
newMessages[1] = prev.asPresentation( newMessages[1] = prev.asPresentation(
resourceProvider = resourceProvider, resourceProvider = resourceProvider,
showDate = false,
showName = false, showName = false,
prevMessage = prevMessage, prevMessage = prevMessage,
nextMessage = messages.value.first(), nextMessage = messages.value.first(),
showTimeInActionMessages = userSettings.showTimeInActionMessages.value showTimeInActionMessages = userSettings.showTimeInActionMessages.value,
conversation = screenState.value.conversation
) )
} }
@@ -208,11 +208,11 @@ class MessagesHistoryViewModelImpl(
?.let { index -> ?.let { index ->
val newMessage = message.asPresentation( val newMessage = message.asPresentation(
resourceProvider = resourceProvider, resourceProvider = resourceProvider,
showDate = false,
showName = false, showName = false,
prevMessage = messages.value.getOrNull(index + 1), prevMessage = messages.value.getOrNull(index + 1),
nextMessage = 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() val newMessages = screenState.value.messages.toMutableList()
@@ -227,7 +227,37 @@ class MessagesHistoryViewModelImpl(
} }
private fun handleReadOutgoingEvent(event: LongPollEvent.VkMessageReadOutgoingEvent) { 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) { private fun loadMessagesHistory(offset: Int = currentOffset.value) {
@@ -239,9 +269,7 @@ class MessagesHistoryViewModelImpl(
offset = offset, offset = offset,
).listenValue(viewModelScope) { state -> ).listenValue(viewModelScope) { state ->
state.processState( state.processState(
error = { error -> error = { error -> },
},
success = { response -> success = { response ->
val messages = response.messages val messages = response.messages
val fullMessages = if (offset == 0) { val fullMessages = if (offset == 0) {
@@ -259,16 +287,6 @@ class MessagesHistoryViewModelImpl(
messagesUseCase.storeMessages(messages) messagesUseCase.storeMessages(messages)
conversationsUseCase.storeConversations(conversations) 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 val itemsCountSufficient = messages.size == MESSAGES_LOAD_COUNT
@@ -281,12 +299,25 @@ class MessagesHistoryViewModelImpl(
conversations conversations
.firstOrNull { it.id == screenState.value.conversationId } .firstOrNull { it.id == screenState.value.conversationId }
?.let { conversation -> ?.let { conversation ->
screenState.setValue { old -> old.copy(conversation = conversation) }
newState = newState.copy( newState = newState.copy(
title = conversation.extractTitle( title = conversation.extractTitle(
useContactName = AppSettings.General.useContactNames, useContactName = AppSettings.General.useContactNames,
resources = resourceProvider.resources 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
) )
} }
@@ -350,18 +381,14 @@ class MessagesHistoryViewModelImpl(
val newMessages = screenState.value.messages.toMutableList() val newMessages = screenState.value.messages.toMutableList()
val newUiMessage = newMessage.asPresentation( val newUiMessage = newMessage.asPresentation(
resourceProvider = resourceProvider, resourceProvider = resourceProvider,
showDate = false,
showName = false, showName = false,
prevMessage = messages.value.firstOrNull(), prevMessage = messages.value.firstOrNull(),
nextMessage = null, nextMessage = null,
showTimeInActionMessages = userSettings.showTimeInActionMessages.value showTimeInActionMessages = userSettings.showTimeInActionMessages.value,
conversation = screenState.value.conversation
) )
newMessages.add(0, newUiMessage) newMessages.add(0, newUiMessage)
messages.setValue { old ->
listOf(newMessage).plus(old)
}
screenState.setValue { old -> screenState.setValue { old ->
old.copy( old.copy(
message = TextFieldValue(), message = TextFieldValue(),
@@ -382,17 +409,22 @@ class MessagesHistoryViewModelImpl(
sendingMessages -= newMessage sendingMessages -= newMessage
}, },
success = { messageId -> 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 -> uiMessages.indexOfOrNull(newUiMessage)?.let { index ->
(messages[index] as? UiItem.Message)?.let { message -> (uiMessages[index] as? UiItem.Message)?.let { message ->
messages[index] = message.copy(id = messageId) 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 -> val uiMessages = messages.mapIndexed { index, item ->
item.asPresentation( item.asPresentation(
resourceProvider = resourceProvider, resourceProvider = resourceProvider,
showDate = false,
showName = false, showName = false,
prevMessage = messages.getOrNull(index + 1), prevMessage = messages.getOrNull(index + 1),
nextMessage = messages.getOrNull(index - 1), nextMessage = messages.getOrNull(index - 1),
showTimeInActionMessages = show showTimeInActionMessages = show,
conversation = screenState.value.conversation
) )
} }
@@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import dev.meloda.fast.common.model.UiImage import dev.meloda.fast.common.model.UiImage
import dev.meloda.fast.model.api.domain.VkAttachment import dev.meloda.fast.model.api.domain.VkAttachment
import dev.meloda.fast.model.api.domain.VkConversation
@Immutable @Immutable
data class MessagesHistoryScreenState( data class MessagesHistoryScreenState(
@@ -18,7 +19,8 @@ data class MessagesHistoryScreenState(
val isPaginating: Boolean, val isPaginating: Boolean,
val isPaginationExhausted: Boolean, val isPaginationExhausted: Boolean,
val actionMode: ActionMode, val actionMode: ActionMode,
val chatImageUrl: String? val chatImageUrl: String?,
val conversation: VkConversation
) { ) {
companion object { companion object {
@@ -34,7 +36,8 @@ data class MessagesHistoryScreenState(
isPaginating = false, isPaginating = false,
isPaginationExhausted = false, isPaginationExhausted = false,
actionMode = ActionMode.Record, actionMode = ActionMode.Record,
chatImageUrl = null chatImageUrl = null,
conversation = VkConversation.EMPTY
) )
} }
} }
@@ -22,7 +22,8 @@ sealed class UiItem(
val showAvatar: Boolean, val showAvatar: Boolean,
val showName: Boolean, val showName: Boolean,
val avatar: UiImage, val avatar: UiImage,
val isEdited: Boolean val isEdited: Boolean,
val isRead: Boolean
) : UiItem(id, conversationMessageId) ) : UiItem(id, conversationMessageId)
data class ActionMessage( data class ActionMessage(
@@ -32,4 +33,3 @@ sealed class UiItem(
val actionCmId: Int? val actionCmId: Int?
) : UiItem(id, conversationMessageId) ) : UiItem(id, conversationMessageId)
} }
@@ -31,6 +31,7 @@ import dev.meloda.fast.messageshistory.model.UiItem
fun IncomingMessageBubble( fun IncomingMessageBubble(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
message: UiItem.Message, message: UiItem.Message,
animate: Boolean
) { ) {
val context = LocalContext.current val context = LocalContext.current
@@ -80,6 +81,8 @@ fun IncomingMessageBubble(
isOut = false, isOut = false,
date = message.date, date = message.date,
edited = message.isEdited, edited = message.isEdited,
animate = animate,
isRead = message.isRead
) )
} }
} }
@@ -1,19 +1,32 @@
package dev.meloda.fast.messageshistory.presentation package dev.meloda.fast.messageshistory.presentation
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box 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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape 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.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.meloda.fast.ui.R as UiR
@Composable @Composable
fun MessageBubble( fun MessageBubble(
@@ -22,6 +35,8 @@ fun MessageBubble(
isOut: Boolean, isOut: Boolean,
date: String?, date: String?,
edited: Boolean, edited: Boolean,
animate: Boolean,
isRead: Boolean
) { ) {
val backgroundColor = if (!isOut) { val backgroundColor = if (!isOut) {
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
@@ -45,44 +60,61 @@ fun MessageBubble(
vertical = 6.dp 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) { if (text != null) {
Text( Text(
text = text, text = text,
modifier = Modifier modifier = Modifier
.padding(2.dp) .padding(2.dp)
.align(Alignment.Center) .align(Alignment.Center)
.animateContentSize(), .padding(end = 4.dp)
.padding(end = dateContainerWidth)
.padding(end = 4.dp)
.then(if (animate) Modifier.animateContentSize() else Modifier),
color = textColor 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( if (isOut) {
// targetValue = if (edited) 50.dp else 30.dp, Icon(
// label = "dateContainerWidth" modifier = Modifier.size(14.dp),
// ) painter = painterResource(
if (isRead) UiR.drawable.round_done_all_24
// AnimatedVisibility( else UiR.drawable.ic_round_done_24
// date != null, ),
// modifier = Modifier contentDescription = null
// .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))
// }
// }
} }
} }
@@ -98,6 +98,7 @@ fun MessagesList(
else Modifier else Modifier
), ),
message = item, message = item,
animate = enableAnimations
) )
} else { } else {
IncomingMessageBubble( IncomingMessageBubble(
@@ -110,6 +111,7 @@ fun MessagesList(
else Modifier else Modifier
), ),
message = item, message = item,
animate = enableAnimations
) )
} }
} }
@@ -3,12 +3,8 @@ package dev.meloda.fast.messageshistory.presentation
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -20,6 +16,7 @@ import dev.meloda.fast.messageshistory.model.UiItem
fun OutgoingMessageBubble( fun OutgoingMessageBubble(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
message: UiItem.Message, message: UiItem.Message,
animate: Boolean
) { ) {
Row( Row(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
@@ -37,18 +34,11 @@ fun OutgoingMessageBubble(
modifier = Modifier, modifier = Modifier,
text = message.text.orDots(), text = message.text.orDots(),
isOut = true, isOut = true,
date = null, date = message.date,
edited = message.isEdited, 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
)
}
} }
} }
} }
@@ -90,8 +90,8 @@ fun VkConversation.extractTitle(
}.parseString(resources).orDots() }.parseString(resources).orDots()
fun VkMessage.asPresentation( fun VkMessage.asPresentation(
conversation: VkConversation,
resourceProvider: ResourceProvider, resourceProvider: ResourceProvider,
showDate: Boolean,
showName: Boolean, showName: Boolean,
prevMessage: VkMessage?, prevMessage: VkMessage?,
nextMessage: VkMessage?, nextMessage: VkMessage?,
@@ -118,11 +118,12 @@ fun VkMessage.asPresentation(
randomId = randomId, randomId = randomId,
isInChat = isPeerChat(), isInChat = isPeerChat(),
name = extractTitle(), name = extractTitle(),
showDate = showDate, showDate = true,
showAvatar = extractShowAvatar(nextMessage), showAvatar = extractShowAvatar(nextMessage),
showName = showName && extractShowName(prevMessage), showName = showName && extractShowName(prevMessage),
avatar = extractAvatar(), avatar = extractAvatar(),
isEdited = updateTime != null isEdited = updateTime != null,
isRead = isRead(conversation)
) )
} }