Прочтение

This commit is contained in:
Халимов Рустам
2026-04-14 15:09:44 +03:00
parent 8165b74e43
commit 118f8b8971
24 changed files with 422 additions and 64 deletions
@@ -33,7 +33,8 @@ data class ChatDetailState(
val searchedGifs: List<KlipyGifDto> = emptyList(),
val recentGifs: List<KlipyGifDto> = emptyList(),
val gifCategories: List<chats.data.remote.api.GifCategoryDto> = emptyList(),
val isGifsLoading: Boolean = false
val isGifsLoading: Boolean = false,
val initialScrollIndex: Int? = null
)
@HiltViewModel
@@ -69,13 +70,39 @@ class ChatDetailViewModel @Inject constructor(
// Ensure SignalR is connected
val token = tokenManager.getToken()
if (token != null) {
val baseUrl = serverConfig.getBaseUrl().removeSuffix("/api/")
val baseUrl = serverConfig.getBaseUrl().removeSuffix("/api")
signalrClient.connect(baseUrl, token)
}
loadChatInfo(chatId)
loadMessages(chatId)
observeMessages(chatId)
observeSignalREvents()
// Initial sync from network
refreshMessages(chatId)
}
private fun observeMessages(chatId: String) {
repository.getMessagesFlow(chatId)
.onEach { messages ->
_state.update { it.copy(messages = messages) }
// Calculate scroll index if not set
if (_state.value.initialScrollIndex == null && messages.isNotEmpty()) {
val firstUnreadIndex = messages.indexOfFirst { !it.isRead && it.senderId != getCurrentUserId() }
val targetIndex = if (firstUnreadIndex != -1) firstUnreadIndex else messages.size - 1
_state.update { it.copy(initialScrollIndex = targetIndex) }
// Automark as read
markAsRead()
}
}
.launchIn(viewModelScope)
}
fun refreshMessages(chatId: String) {
viewModelScope.launch {
repository.getMessages(chatId)
}
}
private fun loadChatInfo(chatId: String) {
@@ -92,17 +119,16 @@ class ChatDetailViewModel @Inject constructor(
}
}
fun loadMessages(chatId: String) {
viewModelScope.launch {
_state.update { it.copy(isLoading = true) }
try {
// Бэкенд обычно возвращает сообщения от новых к старым.
// Для чата нам нужно наоборот: старые вверху, новые внизу.
val messages = repository.getMessages(chatId).reversed()
_state.update { it.copy(messages = messages, isLoading = false) }
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage) }
private fun updateMessageReaction(messageId: String, userId: String, emoji: String) {
_state.update { s ->
val updatedMessages = s.messages.map { msg ->
if (msg.id == messageId) {
val currentReactions = msg.reactions.toMutableMap()
currentReactions[emoji] = (currentReactions[emoji] ?: 0) + 1
msg.copy(reactions = currentReactions)
} else msg
}
s.copy(messages = updatedMessages)
}
}
@@ -119,13 +145,10 @@ class ChatDetailViewModel @Inject constructor(
.onEach { event ->
when (event) {
is ChatEvent.NewMessage -> {
// Avoid adding duplicates if already loaded
val baseUrl = serverConfig.getBaseUrl().removeSuffix("/api/")
_state.update { s ->
val domainMsg = event.message.toDomain(baseUrl)
if (s.messages.none { it.id == domainMsg.id }) {
s.copy(messages = s.messages + domainMsg)
} else s
val domainMsg = event.message.toDomain(baseUrl)
viewModelScope.launch {
repository.saveMessage(domainMsg)
}
}
is ChatEvent.ReactionUpdated -> {
@@ -142,39 +165,53 @@ class ChatDetailViewModel @Inject constructor(
.launchIn(viewModelScope)
}
private fun updateMessageReaction(messageId: String, userId: String, emoji: String) {
_state.update { s ->
val updatedMessages = s.messages.map { msg ->
if (msg.id == messageId) {
// Logic to update reactions map.
// Note: Simplified logic, usually we need to know if it was added or removed.
// If we assume reaction_updated is a toggle:
val currentReactions = msg.reactions.toMutableMap()
val count = currentReactions[emoji] ?: 0
// This is a placeholder logic as the exact behavior depends on server implementation.
// For now, let's just increment/decrement based on some convention or just refresh.
currentReactions[emoji] = count + 1
msg.copy(reactions = currentReactions)
} else msg
fun markAsRead() {
val chatId = currentChatId ?: return
val messages = _state.value.messages
if (messages.isEmpty()) return
val lastMessage = messages.last()
viewModelScope.launch {
try {
repository.markMessagesAsRead(chatId, lastMessage.id, lastMessage.sequenceId)
} catch (e: Exception) {
// Ignore
}
s.copy(messages = updatedMessages)
}
}
fun sendMessage(text: String) {
val chatId = currentChatId ?: return
if (text.isBlank()) return
val tempId = "temp_${System.currentTimeMillis()}"
val userId = getCurrentUserId()
val tempMessage = Message(
id = tempId,
chatId = chatId,
senderId = userId,
content = text,
createdAt = java.util.Date().toString(),
mediaType = chats.domain.model.MediaType.TEXT,
media = emptyList(),
senderName = "Вы",
senderAvatar = null,
reactions = emptyMap(),
isRead = false,
sequenceId = 0
)
viewModelScope.launch {
repository.saveMessage(tempMessage)
}
viewModelScope.launch {
try {
val sentMessage = repository.sendMessage(chatId, text)
// Optimistic update if not already there
_state.update { s ->
if (s.messages.none { it.id == sentMessage.id }) {
s.copy(messages = s.messages + sentMessage)
} else s
}
repository.deleteLocalMessage(tempId)
repository.saveMessage(sentMessage)
} catch (e: Exception) {
repository.deleteLocalMessage(tempId)
_state.update { it.copy(error = e.localizedMessage) }
}
}
@@ -253,6 +290,28 @@ class ChatDetailViewModel @Inject constructor(
fun sendGif(url: String) {
val chatId = currentChatId ?: return
val tempId = "temp_gif_${System.currentTimeMillis()}"
val userId = getCurrentUserId()
val tempMessage = Message(
id = tempId,
chatId = chatId,
senderId = userId,
content = url,
createdAt = java.util.Date().toString(),
mediaType = chats.domain.model.MediaType.IMAGE, // We map GIF to IMAGE for rendering
media = listOf(chats.domain.model.Media(url = url, type = "image", id = "temp_media")),
senderName = "Вы",
senderAvatar = null,
reactions = emptyMap(),
isRead = false,
sequenceId = 0
)
viewModelScope.launch {
repository.saveMessage(tempMessage)
}
viewModelScope.launch {
try {
val attachment = chats.data.remote.api.AttachmentRequest(
@@ -263,12 +322,8 @@ class ChatDetailViewModel @Inject constructor(
)
val sentMessage = repository.sendMessage(chatId, null, "image", listOf(attachment))
// Optimistic update
_state.update { s ->
if (s.messages.none { it.id == sentMessage.id }) {
s.copy(messages = s.messages + sentMessage)
} else s
}
repository.deleteLocalMessage(tempId)
repository.saveMessage(sentMessage)
// Add to recent
val allGifs = _state.value.trendingGifs + _state.value.searchedGifs + _state.value.recentGifs
@@ -287,6 +342,7 @@ class ChatDetailViewModel @Inject constructor(
_state.update { it.copy(recentGifs = newList) }
}
} catch (e: Exception) {
repository.deleteLocalMessage(tempId)
_state.update { it.copy(error = e.localizedMessage) }
}
}