Восстановил поломанную отправку и печать
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.
@@ -33,6 +33,8 @@ class SignalRNotificationObserver @Inject constructor(
|
|||||||
private val chatRepository: chats.domain.repository.ChatRepository,
|
private val chatRepository: chats.domain.repository.ChatRepository,
|
||||||
private val serverConfig: ServerConfig,
|
private val serverConfig: ServerConfig,
|
||||||
private val networkManager: NetworkManager,
|
private val networkManager: NetworkManager,
|
||||||
|
private val messageDao: core.database.data.MessageDao,
|
||||||
|
private val api: chats.data.remote.api.ChatApi,
|
||||||
@ApplicationContext private val context: Context
|
@ApplicationContext private val context: Context
|
||||||
) {
|
) {
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||||
@@ -195,6 +197,7 @@ class SignalRNotificationObserver @Inject constructor(
|
|||||||
/**
|
/**
|
||||||
* Синхронизирует пропущенные сообщения после восстановления соединения
|
* Синхронизирует пропущенные сообщения после восстановления соединения
|
||||||
* Запрашивает только НОВЫЕ сообщения с последнего известного sequenceId
|
* Запрашивает только НОВЫЕ сообщения с последнего известного sequenceId
|
||||||
|
* Показывает уведомления для непрочитанных сообщений
|
||||||
*/
|
*/
|
||||||
private fun syncMissedMessages() {
|
private fun syncMissedMessages() {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -212,11 +215,21 @@ class SignalRNotificationObserver @Inject constructor(
|
|||||||
if (lastSequenceId != null) {
|
if (lastSequenceId != null) {
|
||||||
// Запрашиваем сообщения ПОСЛЕ lastSequenceId (только новые)
|
// Запрашиваем сообщения ПОСЛЕ lastSequenceId (только новые)
|
||||||
android.util.Log.d("SignalRNtfObserver", "Chat ${chat.id}: last known seqId=$lastSequenceId, fetching newer...")
|
android.util.Log.d("SignalRNtfObserver", "Chat ${chat.id}: last known seqId=$lastSequenceId, fetching newer...")
|
||||||
chatRepository.getMessages(
|
val newMessages = chatRepository.getMessages(
|
||||||
chatId = chat.id,
|
chatId = chat.id,
|
||||||
afterSequenceId = lastSequenceId.toLong(),
|
afterSequenceId = lastSequenceId.toLong(),
|
||||||
limit = 100
|
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 {
|
} else {
|
||||||
// Нет локальных сообщений - загружаем последние 50
|
// Нет локальных сообщений - загружаем последние 50
|
||||||
android.util.Log.d("SignalRNtfObserver", "Chat ${chat.id}: no local messages, fetching last 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")
|
android.util.Log.d("SignalRNtfObserver", "Missed messages sync completed")
|
||||||
|
|
||||||
|
// Обновляем счетчик непрочитанных после синхронизации
|
||||||
|
refresh()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e("SignalRNtfObserver", "Sync failed", e)
|
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 для чата из локальной базы
|
* Получает последний известный sequenceId для чата из локальной базы
|
||||||
*/
|
*/
|
||||||
@@ -277,13 +359,101 @@ class SignalRNotificationObserver @Inject constructor(
|
|||||||
android.util.Log.d("SignalRNtfObserver", "SignalR reconnected, syncing missed messages...")
|
android.util.Log.d("SignalRNtfObserver", "SignalR reconnected, syncing missed messages...")
|
||||||
// 5. Синхронизируем пропущенные сообщения
|
// 5. Синхронизируем пропущенные сообщения
|
||||||
syncMissedMessages()
|
syncMissedMessages()
|
||||||
|
|
||||||
|
// 6. Пробуем отправить отложенные сообщения НЕМЕДЛЕННО (не через WorkManager)
|
||||||
|
sendPendingMessagesImmediately()
|
||||||
|
|
||||||
|
// 7. Также планируем WorkManager на всякий случай
|
||||||
|
MessageSyncWorker.scheduleSync(context)
|
||||||
|
android.util.Log.d("SignalRNtfObserver", "Outgoing sync worker scheduled")
|
||||||
} else {
|
} else {
|
||||||
android.util.Log.w("SignalRNtfObserver", "SignalR failed to reconnect within timeout")
|
android.util.Log.w("SignalRNtfObserver", "SignalR failed to reconnect within timeout")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 6. Запускаем синхронизацию отложенных сообщений
|
/**
|
||||||
MessageSyncWorker.scheduleSync(context)
|
* Немедленно отправляет отложенные сообщения без ожидания WorkManager
|
||||||
android.util.Log.d("SignalRNtfObserver", "Outgoing sync worker scheduled")
|
* Вызывается сразу после восстановления соединения
|
||||||
|
*/
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -194,7 +194,35 @@ class ChatRepositoryImpl @Inject constructor(
|
|||||||
|
|
||||||
messageDao.insertMessage(localMessage)
|
messageDao.insertMessage(localMessage)
|
||||||
Log.d(TAG, "Saved local message: $localId")
|
Log.d(TAG, "Saved local message: $localId")
|
||||||
|
|
||||||
|
// Пробуем отправить НЕМЕДЛЕННО через 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)
|
MessageSyncWorker.scheduleSync(context)
|
||||||
|
}
|
||||||
|
|
||||||
// Создаём доменную модель вручную для локального сообщения
|
// Создаём доменную модель вручную для локального сообщения
|
||||||
return Message(
|
return Message(
|
||||||
|
|||||||
@@ -329,6 +329,11 @@ class ChatDetailViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
is ChatEvent.UserTyping -> {
|
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) }
|
_state.update { it.copy(isTyping = true) }
|
||||||
typingTimerJob?.cancel()
|
typingTimerJob?.cancel()
|
||||||
typingTimerJob = viewModelScope.launch {
|
typingTimerJob = viewModelScope.launch {
|
||||||
@@ -337,6 +342,11 @@ class ChatDetailViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
is ChatEvent.UserStoppedTyping -> {
|
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) }
|
_state.update { it.copy(isTyping = false) }
|
||||||
}
|
}
|
||||||
is ChatEvent.MessagesRead -> {
|
is ChatEvent.MessagesRead -> {
|
||||||
|
|||||||
Reference in New Issue
Block a user