- 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 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(
@@ -38,11 +38,10 @@ 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()
@@ -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,12 +299,25 @@ 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
)
}
@@ -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
)
}
@@ -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
)
}
}
@@ -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)
}
@@ -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
@@ -80,6 +81,8 @@ fun IncomingMessageBubble(
isOut = false,
date = message.date,
edited = message.isEdited,
animate = animate,
isRead = message.isRead
)
}
}
@@ -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
)
}
}
}
}
@@ -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
)
}
}
@@ -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,
)
if (message.showDate) {
Spacer(modifier = Modifier.height(2.dp))
Text(
modifier = Modifier.padding(end = 12.dp),
text = message.date,
style = MaterialTheme.typography.labelSmall
animate = animate,
isRead = message.isRead
)
}
}
}
}
@@ -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)
)
}