Исправление работы доставки, прочтения сообщений

This commit is contained in:
Халимов Рустам
2026-04-20 22:17:29 +03:00
parent fc1cf1fd6e
commit 962323814c
13 changed files with 99 additions and 24 deletions
Binary file not shown.
Binary file not shown.
@@ -178,7 +178,13 @@ private fun MessageDto.toEntity(
val reactionsJson = gson.toJson(reactionsMap) val reactionsJson = gson.toJson(reactionsMap)
// Определяем, прочитано ли сообщение текущим пользователем // Определяем, прочитано ли сообщение текущим пользователем
val isRead = senderId == currentUserId // Для своих сообщений: проверяем, прочитал ли кто-то другой (получатели)
// Для чужих сообщений: проверяем, прочитал ли текущий пользователь
val isRead = if (senderId == currentUserId) {
readBy?.any { it.userId != currentUserId } ?: false
} else {
readBy?.any { it.userId == currentUserId } ?: false
}
return MessageEntity( return MessageEntity(
id = id, id = id,
@@ -23,7 +23,12 @@ data class MessageDto(
@SerializedName("replyTo", alternate = ["ReplyTo"]) val replyTo: MessageDto? = null, @SerializedName("replyTo", alternate = ["ReplyTo"]) val replyTo: MessageDto? = null,
@SerializedName("isPinned", alternate = ["IsPinned"]) val isPinned: Boolean? = false, @SerializedName("isPinned", alternate = ["IsPinned"]) val isPinned: Boolean? = false,
@SerializedName("forwardedFromId", alternate = ["ForwardedFromId"]) val forwardedFromId: String? = null, @SerializedName("forwardedFromId", alternate = ["ForwardedFromId"]) val forwardedFromId: String? = null,
@SerializedName("forwardedFrom", alternate = ["ForwardedFrom"]) val forwardedFrom: UserBasicDto? = null @SerializedName("forwardedFrom", alternate = ["ForwardedFrom"]) val forwardedFrom: UserBasicDto? = null,
@SerializedName("readBy", alternate = ["ReadBy"]) val readBy: List<ReadByDto>? = emptyList()
)
data class ReadByDto(
@SerializedName("userId") val userId: String
) )
data class ReactionDto( data class ReactionDto(
@@ -153,7 +153,8 @@ class ChatRepositoryImpl @Inject constructor(
Log.d(TAG, "Cached ${entities.size} messages") Log.d(TAG, "Cached ${entities.size} messages")
} }
messages.map { msg -> msg.toDomain(currentUserId, baseUrl).copy(isRead = true) } // Используем корректную логику из маппера (readBy), а не принудительно true
messages.map { msg -> msg.toDomain(currentUserId, baseUrl) }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Fetch messages failed", e) Log.e(TAG, "Fetch messages failed", e)
emptyList() emptyList()
@@ -186,7 +187,7 @@ class ChatRepositoryImpl @Inject constructor(
mediaType = type.uppercase(), mediaType = type.uppercase(),
mediaJson = "[]", mediaJson = "[]",
reactionsJson = "{}", reactionsJson = "{}",
isRead = true, replyToId = replyToId, isRead = false, replyToId = replyToId,
syncStatus = SyncStatus.SYNCING, syncStatus = SyncStatus.SYNCING,
isDeletedLocally = false, isEditedLocally = false, isDeletedLocally = false, isEditedLocally = false,
editedContent = null, lastUpdated = currentTime editedContent = null, lastUpdated = currentTime
@@ -243,7 +244,7 @@ class ChatRepositoryImpl @Inject constructor(
else -> MediaType.TEXT else -> MediaType.TEXT
}, },
reactions = emptyMap(), reactions = emptyMap(),
isRead = true, isRead = false,
isPinned = false, isPinned = false,
isForwarded = false, isForwarded = false,
forwardedFromName = null, forwardedFromName = null,
+20 -2
View File
@@ -97,6 +97,17 @@ fun MessageDto.toDomain(currentUserId: String, baseUrl: String): Message {
} }
} }
// Исправленная логика isRead:
// Для своих сообщений: проверяем, прочитал ли кто-то другой (получатели)
// Для чужих сообщений: проверяем, прочитал ли текущий пользователь
val isRead = if (senderId == currentUserId) {
// Своё сообщение: прочитано, если кто-то кроме отправителя в readBy
readBy?.any { it.userId != currentUserId } ?: false
} else {
// Чужое сообщение: прочитано, если текущий пользователь в readBy
readBy?.any { it.userId == currentUserId } ?: false
}
return Message( return Message(
id = id, id = id,
chatId = chatId ?: "", chatId = chatId ?: "",
@@ -118,7 +129,7 @@ fun MessageDto.toDomain(currentUserId: String, baseUrl: String): Message {
}, },
mediaType = domainMediaType, mediaType = domainMediaType,
reactions = reactions?.associate { it.emoji to it.count } ?: emptyMap(), reactions = reactions?.associate { it.emoji to it.count } ?: emptyMap(),
isRead = senderId == currentUserId, isRead = isRead,
isPinned = isPinned ?: false, isPinned = isPinned ?: false,
isForwarded = forwardedFromId != null, isForwarded = forwardedFromId != null,
forwardedFromName = forwardedFrom?.displayName ?: forwardedFrom?.username, forwardedFromName = forwardedFrom?.displayName ?: forwardedFrom?.username,
@@ -127,6 +138,13 @@ fun MessageDto.toDomain(currentUserId: String, baseUrl: String): Message {
} }
fun MessageDto.toEntity(baseUrl: String, currentUserId: String, gson: com.google.gson.Gson): core.database.data.MessageEntity { fun MessageDto.toEntity(baseUrl: String, currentUserId: String, gson: com.google.gson.Gson): core.database.data.MessageEntity {
// Исправленная логика isRead для базы данных
val isRead = if (senderId == currentUserId) {
readBy?.any { it.userId != currentUserId } ?: false
} else {
readBy?.any { it.userId == currentUserId } ?: false
}
return core.database.data.MessageEntity( return core.database.data.MessageEntity(
id = id, id = id,
chatId = chatId ?: "", chatId = chatId ?: "",
@@ -139,7 +157,7 @@ fun MessageDto.toEntity(baseUrl: String, currentUserId: String, gson: com.google
mediaType = type ?: "text", mediaType = type ?: "text",
mediaJson = gson.toJson(media), mediaJson = gson.toJson(media),
reactionsJson = gson.toJson(reactions), reactionsJson = gson.toJson(reactions),
isRead = senderId == currentUserId, isRead = isRead,
replyToId = replyTo?.id replyToId = replyTo?.id
) )
} }
@@ -350,10 +350,19 @@ class ChatDetailViewModel @Inject constructor(
_state.update { it.copy(isTyping = false) } _state.update { it.copy(isTyping = false) }
} }
is ChatEvent.MessagesRead -> { is ChatEvent.MessagesRead -> {
val currentUserId = getCurrentUserId()
_state.update { currentState -> _state.update { currentState ->
val updatedMessages = currentState.messages.map { msg -> val updatedMessages = currentState.messages.map { msg ->
if (msg.sequenceId <= event.lastReadSequenceId) { if (msg.sequenceId <= event.lastReadSequenceId) {
msg.copy(isRead = true) // Обновляем isRead на основе readBy
val isRead = if (msg.senderId == currentUserId) {
// Своё сообщение: прочитано, если кто-то кроме отправителя в readBy
event.userId != currentUserId
} else {
// Чужое сообщение: прочитано, если текущий пользователь в readBy
event.userId == currentUserId
}
msg.copy(isRead = isRead)
} else msg } else msg
} }
currentState.copy(messages = updatedMessages) currentState.copy(messages = updatedMessages)
@@ -391,8 +400,9 @@ class ChatDetailViewModel @Inject constructor(
val messages = _state.value.messages val messages = _state.value.messages
if (messages.isEmpty()) return if (messages.isEmpty()) return
// В нашем reverseLayout (newest first) первое сообщение - самое новое от собеседника
val currentUserId = getCurrentUserId() val currentUserId = getCurrentUserId()
// Находим последнее сообщение от собеседника
val lastMessageFromOther = messages.firstOrNull { it.senderId != currentUserId } ?: return val lastMessageFromOther = messages.firstOrNull { it.senderId != currentUserId } ?: return
viewModelScope.launch { viewModelScope.launch {
@@ -400,16 +410,21 @@ class ChatDetailViewModel @Inject constructor(
// Мгновенно обновляем в памяти для "галочек" // Мгновенно обновляем в памяти для "галочек"
_state.update { currentState -> _state.update { currentState ->
val updatedMessages = currentState.messages.map { msg -> val updatedMessages = currentState.messages.map { msg ->
// Для чужих сообщений помечаем как прочитанные, если sequenceId <= последнего сообщения от собеседника
if (msg.senderId != currentUserId && msg.sequenceId <= lastMessageFromOther.sequenceId) { if (msg.senderId != currentUserId && msg.sequenceId <= lastMessageFromOther.sequenceId) {
msg.copy(isRead = true) msg.copy(isRead = true)
} else msg } else msg
} }
currentState.copy(messages = updatedMessages) currentState.copy(messages = updatedMessages)
} }
// Отправляем на сервер
repository.markMessagesAsRead(chatId, lastMessageFromOther.id, lastMessageFromOther.sequenceId) repository.markMessagesAsRead(chatId, lastMessageFromOther.id, lastMessageFromOther.sequenceId)
// Обновляем через SignalR observer для получения актуальных данных
signalrNotificationObserver.refresh() signalrNotificationObserver.refresh()
} catch (e: Exception) { } catch (e: Exception) {
// Ignore android.util.Log.e("ChatDetailVM", "markAsRead failed", e)
} }
} }
} }
@@ -16,15 +16,21 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import chats.domain.model.Chat import chats.domain.model.Chat
import chats.domain.model.Message
import chats.domain.model.MediaType import chats.domain.model.MediaType
import core.presentation.components.AppAvatar import core.presentation.components.AppAvatar
import ru.knot.messager.R import ru.knot.messager.R
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.filled.DoneAll
@Composable @Composable
fun ChatItem( fun ChatItem(
chat: Chat, chat: Chat,
onClick: (String) -> Unit onClick: (String) -> Unit
) { ) {
val currentUserId = "current_user_id" // TODO: Get from AuthManager/TokenManager
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -100,20 +106,44 @@ fun ChatItem(
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
if (chat.unreadCount > 0) { Row(
Box( verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier.padding(start = 8.dp)
.padding(start = 8.dp) ) {
.background(MaterialTheme.colorScheme.primary, CircleShape) // Галочки прочтения только для своих сообщений
.padding(horizontal = 6.dp, vertical = 2.dp), chat.lastMessage?.let { lastMessage ->
contentAlignment = Alignment.Center if (lastMessage.senderId == currentUserId && !lastMessage.isRead) {
) { Icon(
Text( imageVector = Icons.Default.Done,
text = chat.unreadCount.toString(), contentDescription = null,
color = Color.White, tint = Color.Gray,
fontSize = 10.sp, modifier = Modifier.size(14.dp)
fontWeight = FontWeight.Bold )
) } else if (lastMessage.senderId == currentUserId && lastMessage.isRead) {
Icon(
imageVector = Icons.Default.DoneAll,
contentDescription = null,
tint = Color.Blue,
modifier = Modifier.size(14.dp)
)
}
}
if (chat.unreadCount > 0) {
Spacer(modifier = Modifier.width(4.dp))
Box(
modifier = Modifier
.background(MaterialTheme.colorScheme.primary, CircleShape)
.padding(horizontal = 6.dp, vertical = 2.dp),
contentAlignment = Alignment.Center
) {
Text(
text = chat.unreadCount.toString(),
color = Color.White,
fontSize = 10.sp,
fontWeight = FontWeight.Bold
)
}
} }
} }
} }