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

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

View File

@@ -178,7 +178,13 @@ private fun MessageDto.toEntity(
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(
id = id,

View File

@@ -23,7 +23,12 @@ data class MessageDto(
@SerializedName("replyTo", alternate = ["ReplyTo"]) val replyTo: MessageDto? = null,
@SerializedName("isPinned", alternate = ["IsPinned"]) val isPinned: Boolean? = false,
@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(

View File

@@ -153,7 +153,8 @@ class ChatRepositoryImpl @Inject constructor(
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) {
Log.e(TAG, "Fetch messages failed", e)
emptyList()
@@ -186,7 +187,7 @@ class ChatRepositoryImpl @Inject constructor(
mediaType = type.uppercase(),
mediaJson = "[]",
reactionsJson = "{}",
isRead = true, replyToId = replyToId,
isRead = false, replyToId = replyToId,
syncStatus = SyncStatus.SYNCING,
isDeletedLocally = false, isEditedLocally = false,
editedContent = null, lastUpdated = currentTime
@@ -243,7 +244,7 @@ class ChatRepositoryImpl @Inject constructor(
else -> MediaType.TEXT
},
reactions = emptyMap(),
isRead = true,
isRead = false,
isPinned = false,
isForwarded = false,
forwardedFromName = null,

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(
id = id,
chatId = chatId ?: "",
@@ -118,7 +129,7 @@ fun MessageDto.toDomain(currentUserId: String, baseUrl: String): Message {
},
mediaType = domainMediaType,
reactions = reactions?.associate { it.emoji to it.count } ?: emptyMap(),
isRead = senderId == currentUserId,
isRead = isRead,
isPinned = isPinned ?: false,
isForwarded = forwardedFromId != null,
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 {
// Исправленная логика isRead для базы данных
val isRead = if (senderId == currentUserId) {
readBy?.any { it.userId != currentUserId } ?: false
} else {
readBy?.any { it.userId == currentUserId } ?: false
}
return core.database.data.MessageEntity(
id = id,
chatId = chatId ?: "",
@@ -139,7 +157,7 @@ fun MessageDto.toEntity(baseUrl: String, currentUserId: String, gson: com.google
mediaType = type ?: "text",
mediaJson = gson.toJson(media),
reactionsJson = gson.toJson(reactions),
isRead = senderId == currentUserId,
isRead = isRead,
replyToId = replyTo?.id
)
}

View File

@@ -350,10 +350,19 @@ class ChatDetailViewModel @Inject constructor(
_state.update { it.copy(isTyping = false) }
}
is ChatEvent.MessagesRead -> {
val currentUserId = getCurrentUserId()
_state.update { currentState ->
val updatedMessages = currentState.messages.map { msg ->
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
}
currentState.copy(messages = updatedMessages)
@@ -391,8 +400,9 @@ class ChatDetailViewModel @Inject constructor(
val messages = _state.value.messages
if (messages.isEmpty()) return
// В нашем reverseLayout (newest first) первое сообщение - самое новое от собеседника
val currentUserId = getCurrentUserId()
// Находим последнее сообщение от собеседника
val lastMessageFromOther = messages.firstOrNull { it.senderId != currentUserId } ?: return
viewModelScope.launch {
@@ -400,16 +410,21 @@ class ChatDetailViewModel @Inject constructor(
// Мгновенно обновляем в памяти для "галочек"
_state.update { currentState ->
val updatedMessages = currentState.messages.map { msg ->
// Для чужих сообщений помечаем как прочитанные, если sequenceId <= последнего сообщения от собеседника
if (msg.senderId != currentUserId && msg.sequenceId <= lastMessageFromOther.sequenceId) {
msg.copy(isRead = true)
} else msg
}
currentState.copy(messages = updatedMessages)
}
// Отправляем на сервер
repository.markMessagesAsRead(chatId, lastMessageFromOther.id, lastMessageFromOther.sequenceId)
// Обновляем через SignalR observer для получения актуальных данных
signalrNotificationObserver.refresh()
} catch (e: Exception) {
// Ignore
android.util.Log.e("ChatDetailVM", "markAsRead failed", e)
}
}
}

View File

@@ -16,15 +16,21 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.runtime.remember
import chats.domain.model.Chat
import chats.domain.model.Message
import chats.domain.model.MediaType
import core.presentation.components.AppAvatar
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
fun ChatItem(
chat: Chat,
onClick: (String) -> Unit
) {
val currentUserId = "current_user_id" // TODO: Get from AuthManager/TokenManager
Row(
modifier = Modifier
.fillMaxWidth()
@@ -100,20 +106,44 @@ fun ChatItem(
modifier = Modifier.weight(1f)
)
if (chat.unreadCount > 0) {
Box(
modifier = Modifier
.padding(start = 8.dp)
.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
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(start = 8.dp)
) {
// Галочки прочтения только для своих сообщений
chat.lastMessage?.let { lastMessage ->
if (lastMessage.senderId == currentUserId && !lastMessage.isRead) {
Icon(
imageVector = Icons.Default.Done,
contentDescription = null,
tint = Color.Gray,
modifier = Modifier.size(14.dp)
)
} 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
)
}
}
}
}