Восстановил поломанную отправку и печать

This commit is contained in:
Халимов Рустам
2026-04-20 12:06:43 +03:00
parent b65c8f3633
commit c592197017
10 changed files with 214 additions and 6 deletions

View File

@@ -33,6 +33,8 @@ class SignalRNotificationObserver @Inject constructor(
private val chatRepository: chats.domain.repository.ChatRepository,
private val serverConfig: ServerConfig,
private val networkManager: NetworkManager,
private val messageDao: core.database.data.MessageDao,
private val api: chats.data.remote.api.ChatApi,
@ApplicationContext private val context: Context
) {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
@@ -195,6 +197,7 @@ class SignalRNotificationObserver @Inject constructor(
/**
* Синхронизирует пропущенные сообщения после восстановления соединения
* Запрашивает только НОВЫЕ сообщения с последнего известного sequenceId
* Показывает уведомления для непрочитанных сообщений
*/
private fun syncMissedMessages() {
scope.launch {
@@ -212,11 +215,21 @@ class SignalRNotificationObserver @Inject constructor(
if (lastSequenceId != null) {
// Запрашиваем сообщения ПОСЛЕ lastSequenceId (только новые)
android.util.Log.d("SignalRNtfObserver", "Chat ${chat.id}: last known seqId=$lastSequenceId, fetching newer...")
chatRepository.getMessages(
val newMessages = chatRepository.getMessages(
chatId = chat.id,
afterSequenceId = lastSequenceId.toLong(),
limit = 100
)
android.util.Log.d("SignalRNtfObserver", "Chat ${chat.id}: fetched ${newMessages.size} messages from API")
// Показываем уведомление если есть новые сообщения и чат не открыт
if (newMessages.isNotEmpty() && activeChatTracker.currentChatId.value != chat.id) {
android.util.Log.d("SignalRNtfObserver", "Chat ${chat.id}: ${newMessages.size} new messages, showing notification")
showMissedMessagesNotification(chat, newMessages)
} else {
android.util.Log.d("SignalRNtfObserver", "Chat ${chat.id}: skipping notification (messages=${newMessages.size}, chatOpen=${activeChatTracker.currentChatId.value == chat.id})")
}
} else {
// Нет локальных сообщений - загружаем последние 50
android.util.Log.d("SignalRNtfObserver", "Chat ${chat.id}: no local messages, fetching last 50")
@@ -232,12 +245,81 @@ class SignalRNotificationObserver @Inject constructor(
}
android.util.Log.d("SignalRNtfObserver", "Missed messages sync completed")
// Обновляем счетчик непрочитанных после синхронизации
refresh()
} catch (e: Exception) {
android.util.Log.e("SignalRNtfObserver", "Sync failed", e)
}
}
}
/**
* Показывает уведомление о пропущенных сообщениях
*/
private fun showMissedMessagesNotification(chat: chats.domain.model.Chat, newMessages: List<chats.domain.model.Message>) {
scope.launch {
try {
android.util.Log.d("SignalRNtfObserver", "showMissedMessagesNotification called for chat ${chat.id} with ${newMessages.size} messages")
// Фильтруем сообщения не от текущего пользователя
val currentUserId = tokenManager.getUserId()
android.util.Log.d("SignalRNtfObserver", "Current user ID: $currentUserId")
val messagesFromOthers = newMessages.filter { it.senderId != currentUserId }
android.util.Log.d("SignalRNtfObserver", "Messages from others: ${messagesFromOthers.size}")
if (messagesFromOthers.isEmpty()) {
android.util.Log.d("SignalRNtfObserver", "No new messages from others in chat ${chat.id} - all messages are from current user")
return@launch
}
// Группируем сообщения по отправителям
val messagesBySender = messagesFromOthers.groupBy { it.senderId }
android.util.Log.d("SignalRNtfObserver", "Messages grouped by ${messagesBySender.size} sender(s)")
// Для каждого отправителя показываем уведомление
messagesBySender.forEach { (senderId, messages) ->
val senderName = messages.firstOrNull()?.senderName ?: "Контакт"
val messageCount = messages.size
android.util.Log.d("SignalRNtfObserver", "Processing sender $senderName with $messageCount messages")
// Формируем текст уведомления
val notificationText = when {
messageCount == 1 -> {
messages.firstOrNull()?.content ?: "Новое сообщение"
}
messageCount <= 3 -> {
messages.take(3).mapNotNull { it.content }.joinToString(", ")
}
else -> {
"$messageCount новых сообщений"
}
}
android.util.Log.d("SignalRNtfObserver", "Showing notification for chat ${chat.id}: $senderName - $notificationText")
// Небольшая задержка перед показом уведомления
kotlinx.coroutines.delay(500)
// Показываем уведомление
NotificationHelper.showNotification(
context = context,
title = senderName,
body = notificationText,
type = "chat",
chatId = chat.id,
notificationId = chat.id.hashCode(),
totalCount = activeChatTracker.totalUnreadCount.value
)
}
} catch (e: Exception) {
android.util.Log.e("SignalRNtfObserver", "Failed to show missed messages notification", e)
}
}
}
/**
* Получает последний известный sequenceId для чата из локальной базы
*/
@@ -277,13 +359,101 @@ class SignalRNotificationObserver @Inject constructor(
android.util.Log.d("SignalRNtfObserver", "SignalR reconnected, syncing missed messages...")
// 5. Синхронизируем пропущенные сообщения
syncMissedMessages()
// 6. Пробуем отправить отложенные сообщения НЕМЕДЛЕННО (не через WorkManager)
sendPendingMessagesImmediately()
// 7. Также планируем WorkManager на всякий случай
MessageSyncWorker.scheduleSync(context)
android.util.Log.d("SignalRNtfObserver", "Outgoing sync worker scheduled")
} else {
android.util.Log.w("SignalRNtfObserver", "SignalR failed to reconnect within timeout")
}
// 6. Запускаем синхронизацию отложенных сообщений
MessageSyncWorker.scheduleSync(context)
android.util.Log.d("SignalRNtfObserver", "Outgoing sync worker scheduled")
}
}
/**
* Немедленно отправляет отложенные сообщения без ожидания WorkManager
* Вызывается сразу после восстановления соединения
*/
private fun sendPendingMessagesImmediately() {
scope.launch {
try {
android.util.Log.d("SignalRNtfObserver", "Checking for pending messages to send...")
// Получаем все сообщения со статусом SYNCING
val pendingMessages = messageDao.getPendingSyncMessages()
android.util.Log.d("SignalRNtfObserver", "Found ${pendingMessages.size} pending messages")
if (pendingMessages.isEmpty()) {
android.util.Log.d("SignalRNtfObserver", "No pending messages to send")
return@launch
}
var successCount = 0
var failureCount = 0
for (message in pendingMessages) {
try {
android.util.Log.d("SignalRNtfObserver", "Sending pending message: ${message.id}")
val attachments = parseAttachments(message.mediaJson)
val request = chats.data.remote.api.SendMessageRequest(
content = message.content,
type = message.mediaType.lowercase(),
attachments = attachments,
replyToId = message.replyToId
)
val response = api.sendMessage(message.chatId, request)
// Обновляем сообщение в базе
val syncedMessage = message.copy(
id = response.id,
sequenceId = response.sequenceId ?: message.sequenceId,
createdAt = response.createdAt ?: message.createdAt,
syncStatus = core.database.data.SyncStatus.SYNCED,
isDeletedLocally = false,
isEditedLocally = false,
editedContent = null,
lastUpdated = System.currentTimeMillis()
)
messageDao.insertMessage(syncedMessage)
android.util.Log.d("SignalRNtfObserver", "Message sent successfully: ${response.id}")
successCount++
} catch (e: Exception) {
android.util.Log.e("SignalRNtfObserver", "Failed to send message ${message.id}", e)
failureCount++
}
}
android.util.Log.d("SignalRNtfObserver", "Pending messages sync completed. Success: $successCount, Failed: $failureCount")
} catch (e: Exception) {
android.util.Log.e("SignalRNtfObserver", "sendPendingMessagesImmediately failed", e)
}
}
}
private fun parseAttachments(mediaJson: String): List<chats.data.remote.api.AttachmentRequest>? {
return try {
val gson = com.google.gson.Gson()
val mediaList = gson.fromJson(mediaJson, Array::class.java)
?.map { elem ->
val map = elem as Map<*, *>
chats.data.remote.api.AttachmentRequest(
type = map["type"] as? String ?: "file",
url = map["url"] as? String ?: "",
fileName = map["filename"] as? String ?: "file",
fileSize = (map["size"] as? Number)?.toLong() ?: 0L
)
}
mediaList?.takeIf { it.isNotEmpty() }
} catch (e: Exception) {
android.util.Log.e("SignalRNtfObserver", "Failed to parse attachments", e)
null
}
}

View File

@@ -194,7 +194,35 @@ class ChatRepositoryImpl @Inject constructor(
messageDao.insertMessage(localMessage)
Log.d(TAG, "Saved local message: $localId")
MessageSyncWorker.scheduleSync(context)
// Пробуем отправить НЕМЕДЛЕННО через API
try {
Log.d(TAG, "Sending message immediately via API: $localId")
val request = SendMessageRequest(
content = content,
type = type,
attachments = attachments,
replyToId = replyToId
)
val response = api.sendMessage(chatId, request)
Log.d(TAG, "Message sent successfully: ${response.id}")
// Обновляем сообщение в базе с серверными данными
val syncedMessage = localMessage.copy(
id = response.id,
sequenceId = response.sequenceId ?: 0,
createdAt = response.createdAt ?: localMessage.createdAt,
syncStatus = SyncStatus.SYNCED
)
messageDao.insertMessage(syncedMessage)
// Возвращаем доменную модель с серверными данными
return response.toDomain(userId, baseUrl)
} catch (e: Exception) {
Log.e(TAG, "Failed to send message immediately, scheduling sync: ${e.message}")
// Ошибка - планируем синхронизацию через WorkManager
MessageSyncWorker.scheduleSync(context)
}
// Создаём доменную модель вручную для локального сообщения
return Message(

View File

@@ -329,6 +329,11 @@ class ChatDetailViewModel @Inject constructor(
}
}
is ChatEvent.UserTyping -> {
// Игнорируем свои же typing события
if (event.userId == getCurrentUserId()) {
android.util.Log.d("ChatDetailVM", "Ignoring own typing event")
return@onEach
}
_state.update { it.copy(isTyping = true) }
typingTimerJob?.cancel()
typingTimerJob = viewModelScope.launch {
@@ -337,6 +342,11 @@ class ChatDetailViewModel @Inject constructor(
}
}
is ChatEvent.UserStoppedTyping -> {
// Игнорируем свои же события
if (event.userId == getCurrentUserId()) {
android.util.Log.d("ChatDetailVM", "Ignoring own stopped typing event")
return@onEach
}
_state.update { it.copy(isTyping = false) }
}
is ChatEvent.MessagesRead -> {