Рабочие счетчики и переработка чата

This commit is contained in:
Халимов Рустам
2026-04-15 01:14:55 +03:00
parent 58fdf1aca1
commit 487cb1b12b
18 changed files with 725 additions and 292 deletions
@@ -11,6 +11,7 @@ import chats.domain.model.Message
import chats.domain.model.MediaType
import chats.domain.repository.ChatRepository
import core.network.ServerConfig
import chats.data.remote.signalr.ReadMessagesRequest
import core.security.TokenManager
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
@@ -40,15 +41,27 @@ class ChatRepositoryImpl @Inject constructor(
}
}
override suspend fun getMessages(chatId: String): List<Message> {
override suspend fun getMessages(chatId: String, cursor: String?, limit: Int?): List<Message> {
val baseUrl = serverConfig.getBaseUrl().removeSuffix("/api/")
val currentUserId = tokenManager.getUserId() ?: ""
return try {
val messages = api.getMessages(chatId)
// Save to DB
messageDao.insertMessages(messages.map { msg: MessageDto -> msg.toEntity(baseUrl, gson) })
messages.map { it.toDomain(baseUrl) }
val chats = api.getChats()
val chatDto = chats.find { it.id == chatId }
val unreadCount = chatDto?.unreadCount ?: 0
android.util.Log.d("ChatRepo", "Fetching messages from network for chat: $chatId, cursor: $cursor")
val messages = api.getMessages(chatId, cursor = cursor, limit = limit)
if (messages.isNotEmpty()) {
android.util.Log.d("ChatRepo", "Received ${messages.size} messages. First: ${messages.first().createdAt}, Last: ${messages.last().createdAt}")
}
// Определяем, какие сообщения считаются прочитанными
val mappedMessages = messages.mapIndexed { index, msg ->
val isUnread = (messages.size - index) <= unreadCount && msg.senderId != currentUserId
msg.toDomain(currentUserId, baseUrl).copy(isRead = !isUnread)
}
mappedMessages
} catch (e: Exception) {
// If network fails, caller should ideally use the Flow from DB
emptyList()
}
}
@@ -65,7 +78,8 @@ class ChatRepositoryImpl @Inject constructor(
attachments = attachments
)
val baseUrl = serverConfig.getBaseUrl().removeSuffix("/api/")
return api.sendMessage(chatId, request).toDomain(baseUrl)
val userId = tokenManager.getUserId() ?: ""
return api.sendMessage(chatId, request).toDomain(userId, baseUrl)
}
override suspend fun addReaction(messageId: String, emoji: String) {
@@ -77,16 +91,18 @@ class ChatRepositoryImpl @Inject constructor(
}
override suspend fun markMessagesAsRead(chatId: String, lastMessageId: String, lastReadSequenceId: Int) {
val request = chats.data.remote.signalr.ReadMessagesRequest(
chatId = chatId,
lastReadMessageId = lastMessageId,
lastReadSequenceId = lastReadSequenceId
)
hubClient.readMessages(request)
try {
android.util.Log.d("ChatRepoImpl", "markMessagesAsRead CALLED FOR $chatId. Caller stack: ${android.util.Log.getStackTraceString(Throwable())}")
hubClient.readMessages(ReadMessagesRequest(chatId, lastMessageId, lastReadSequenceId))
} catch (e: Exception) {
android.util.Log.e("ChatRepo", "Error marking messages as read", e)
}
// Обновляем локальную БД
messageDao.markMessagesAsRead(chatId, lastReadSequenceId)
}
override suspend fun saveMessage(message: Message) {
messageDao.insertMessages(listOf(message.toEntity(gson)))
android.util.Log.d("ChatRepo", "DB cache disabled, skipping save: ${message.id}")
}
override suspend fun deleteLocalMessage(messageId: String) {
@@ -119,168 +135,3 @@ class ChatRepositoryImpl @Inject constructor(
return api.getGifCategories().data.categories
}
}
// Mappers
fun ChatDto.toDomain(currentUserId: String, baseUrl: String): Chat {
val chatName = name ?: if (type == "personal") {
members.firstOrNull { it.userId != currentUserId }?.user?.displayName ?: "Unknown Chat"
} else "Group Chat"
val chatAvatar = (avatar ?: if (type == "personal") {
members.firstOrNull { it.userId != currentUserId }?.user?.avatarUrl
} else null)?.ensureAbsoluteUrl(baseUrl)
return Chat(
id = id,
type = type,
name = chatName,
avatar = chatAvatar,
unreadCount = unreadCount,
lastMessage = messages.firstOrNull()?.toDomain(baseUrl)
)
}
fun Message.toEntity(gson: com.google.gson.Gson): core.database.data.MessageEntity {
return core.database.data.MessageEntity(
id = id,
chatId = chatId,
senderId = senderId,
senderName = senderName,
senderAvatar = senderAvatar,
content = content,
sequenceId = sequenceId,
createdAt = createdAt,
mediaType = mediaType.name.lowercase(),
mediaJson = gson.toJson(media),
reactionsJson = gson.toJson(reactions),
isRead = isRead,
replyToId = replyTo?.id
)
}
fun MessageDto.toDomain(baseUrl: String): Message {
val domainMediaType = when (type) {
"gif" -> MediaType.GIF
"image", "photo" -> MediaType.IMAGE
"video" -> MediaType.VIDEO
"audio", "voice" -> MediaType.AUDIO
"file" -> MediaType.FILE
else -> when (media.firstOrNull()?.type) {
"image", "photo" -> MediaType.IMAGE
"video" -> MediaType.VIDEO
"audio", "voice" -> MediaType.AUDIO
"file" -> MediaType.FILE
else -> MediaType.TEXT
}
}
return Message(
id = id,
chatId = chatId ?: "",
senderId = senderId ?: "",
senderName = sender?.displayName ?: "Unknown",
senderAvatar = sender?.avatarUrl?.ensureAbsoluteUrl(baseUrl),
content = content,
sequenceId = sequenceId ?: 0,
createdAt = createdAt ?: "",
media = media.map {
chats.domain.model.Media(
id = it.id ?: java.util.UUID.randomUUID().toString(),
type = it.type ?: "unknown",
url = (it.url ?: "").ensureAbsoluteUrl(baseUrl),
filename = it.filename,
size = it.size,
duration = it.duration
)
},
mediaType = domainMediaType,
reactions = reactions?.associate { it.emoji to it.count } ?: emptyMap(),
isRead = true, // Network messages are usually considered read when fetched or handled by server
replyTo = replyTo?.toDomain(baseUrl)
)
}
fun MessageDto.toEntity(baseUrl: String, gson: com.google.gson.Gson): core.database.data.MessageEntity {
return core.database.data.MessageEntity(
id = id,
chatId = chatId ?: "",
senderId = senderId ?: "",
senderName = sender?.displayName ?: "Unknown",
senderAvatar = sender?.avatarUrl?.ensureAbsoluteUrl(baseUrl),
content = content,
sequenceId = sequenceId ?: 0,
createdAt = createdAt ?: "",
mediaType = type ?: "text",
mediaJson = gson.toJson(media),
reactionsJson = gson.toJson(reactions),
isRead = true,
replyToId = replyTo?.id
)
}
fun core.database.data.MessageEntity.toDomain(baseUrl: String, gson: com.google.gson.Gson): Message {
val mediaTypeEnum = when (mediaType) {
"gif" -> MediaType.GIF
"image", "photo" -> MediaType.IMAGE
"video" -> MediaType.VIDEO
"audio", "voice" -> MediaType.AUDIO
"file" -> MediaType.FILE
else -> MediaType.TEXT
}
val mediaTypeToken = object : com.google.gson.reflect.TypeToken<List<chats.data.remote.dto.MediaItemDto>>() {}.type
val mediaDtos: List<chats.data.remote.dto.MediaItemDto> = try {
gson.fromJson(mediaJson, mediaTypeToken)
} catch (e: Exception) {
emptyList()
} ?: emptyList()
val reactionsTypeToken = object : com.google.gson.reflect.TypeToken<List<chats.data.remote.dto.ReactionDto>>() {}.type
val reactionDtos: List<chats.data.remote.dto.ReactionDto> = try {
// Try to parse as array (List<ReactionDto>)
gson.fromJson(reactionsJson, reactionsTypeToken)
} catch (e: Exception) {
// If it's an object instead of array, parse as map and convert to list
try {
val mapType = object : com.google.gson.reflect.TypeToken<Map<String, Int>>() {}.type
val map: Map<String, Int> = gson.fromJson(reactionsJson, mapType) ?: emptyMap()
map.map { chats.data.remote.dto.ReactionDto(it.key, it.value, false) }
} catch (innerE: Exception) {
emptyList()
}
} ?: emptyList()
return Message(
id = id,
chatId = chatId,
senderId = senderId,
senderName = senderName,
senderAvatar = senderAvatar,
content = content,
sequenceId = sequenceId,
createdAt = createdAt,
media = mediaDtos.map {
chats.domain.model.Media(
id = it.id ?: java.util.UUID.randomUUID().toString(),
type = it.type ?: "unknown",
url = (it.url ?: "").ensureAbsoluteUrl(baseUrl),
filename = it.filename,
size = it.size,
duration = it.duration
)
},
mediaType = mediaTypeEnum,
reactions = reactionDtos.associate { it.emoji to it.count },
isRead = isRead
)
}
fun String.ensureAbsoluteUrl(baseUrl: String): String {
return if (this.startsWith("http")) {
this
} else {
val base = baseUrl.removeSuffix("/")
val path = if (this.startsWith("/")) this else "/$this"
"$base$path"
}
}