forked from melod1n/fast-messenger
- read indicator, edit status and time for message in messages history
This commit is contained in:
@@ -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()
|
||||
|
||||
+66
-34
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+5
-2
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+60
-28
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+4
-14
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user