Исправление работы доставки, прочтения сообщений
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user