feat: Bump version to 1.0.7, enhance message delivery handling, and add connection logs screen

This commit is contained in:
2026-02-25 11:16:31 +05:00
parent 75810a0696
commit aed685ee73
11 changed files with 618 additions and 59 deletions

View File

@@ -96,6 +96,9 @@ class MessageRepository private constructor(private val context: Context) {
companion object {
@Volatile private var INSTANCE: MessageRepository? = null
/** Desktop parity: MESSAGE_MAX_TIME_TO_DELEVERED_S = 80 (seconds) */
private const val MESSAGE_MAX_TIME_TO_DELIVERED_MS = 80_000L
const val SYSTEM_SAFE_PUBLIC_KEY = "0x000000000000000000000000000000000000000002"
const val SYSTEM_SAFE_TITLE = "Safe"
const val SYSTEM_SAFE_USERNAME = "safe"
@@ -773,6 +776,9 @@ class MessageRepository private constructor(private val context: Context) {
)
// 🔥 Запрашиваем информацию о пользователе для отображения имени вместо ключа
// Desktop parity: always re-fetch on incoming message so renamed contacts
// get their new name/username updated in the chat list.
requestedUserInfoKeys.remove(dialogOpponentKey)
requestUserInfo(dialogOpponentKey)
// Обновляем кэш только если сообщение новое
@@ -810,11 +816,22 @@ class MessageRepository private constructor(private val context: Context) {
status = "DELIVERED"
)
messageDao.updateDeliveryStatus(account, packet.messageId, DeliveryStatus.DELIVERED.value)
// Desktop parity: update both delivery status AND timestamp on delivery confirmation.
// Desktop sets timestamp = Date.now() when PacketDelivery arrives (useSynchronize.ts).
val deliveryTimestamp = System.currentTimeMillis()
messageDao.updateDeliveryStatusAndTimestamp(
account, packet.messageId, DeliveryStatus.DELIVERED.value, deliveryTimestamp
)
// Обновляем кэш
// Обновляем кэш (status + timestamp)
val dialogKey = getDialogKey(packet.toPublicKey)
updateMessageStatus(dialogKey, packet.messageId, DeliveryStatus.DELIVERED)
messageCache[dialogKey]?.let { flow ->
flow.value = flow.value.map { msg ->
if (msg.messageId == packet.messageId)
msg.copy(deliveryStatus = DeliveryStatus.DELIVERED, timestamp = deliveryTimestamp)
else msg
}
}
// 🔔 Уведомляем UI о смене статуса доставки
_deliveryStatusEvents.tryEmit(
@@ -950,6 +967,86 @@ class MessageRepository private constructor(private val context: Context) {
}
}
// ===============================
// Retry WAITING messages on reconnect
// ===============================
/**
* Desktop parity: resend messages stuck in WAITING status.
*
* On Android, if the app is killed while a message is being sent, it stays in DB
* with delivered=WAITING forever. Desktop has _packetQueue (in-memory) but desktop
* apps are rarely force-killed. On Android this is critical.
*
* Messages older than MESSAGE_MAX_TIME_TO_DELIVERED_MS are marked as ERROR instead
* of being retried (desktop: MESSAGE_MAX_TIME_TO_DELEVERED_S = 80s).
*/
suspend fun retryWaitingMessages() {
val account = currentAccount ?: return
val privateKey = currentPrivateKey ?: return
val now = System.currentTimeMillis()
// Mark expired messages as ERROR (older than 80 seconds)
val expiredCount = messageDao.markExpiredWaitingAsError(account, now - MESSAGE_MAX_TIME_TO_DELIVERED_MS)
if (expiredCount > 0) {
android.util.Log.w("MessageRepository", "⚠️ Marked $expiredCount expired WAITING messages as ERROR")
}
// Get remaining WAITING messages (younger than 80s)
val waitingMessages = messageDao.getWaitingMessages(account, now - MESSAGE_MAX_TIME_TO_DELIVERED_MS)
if (waitingMessages.isEmpty()) return
android.util.Log.i("MessageRepository", "🔄 Retrying ${waitingMessages.size} WAITING messages")
for (entity in waitingMessages) {
// Skip saved messages (should not happen, but guard)
if (entity.fromPublicKey == entity.toPublicKey) continue
try {
// The message is already saved in DB with encrypted content and chachaKey.
// We can re-send the PacketMessage directly using stored fields.
val aesChachaKeyValue = if (entity.chachaKey.startsWith("sync:")) {
entity.chachaKey.removePrefix("sync:")
} else {
// Re-generate aesChachaKey from the stored chachaKey + privateKey.
// The chachaKey in DB is the ECC-encrypted key for the recipient.
// We need to decrypt it, then re-encrypt with our private key for self-sync.
try {
val plainKeyAndNonce = MessageCrypto.decryptKeyFromSender(entity.chachaKey, privateKey)
CryptoManager.encryptWithPassword(
String(plainKeyAndNonce, Charsets.ISO_8859_1),
privateKey
)
} catch (e: Exception) {
android.util.Log.w("MessageRepository", "⚠️ Cannot regenerate aesChachaKey for ${entity.messageId.take(8)}, sending without it")
""
}
}
val packet = PacketMessage().apply {
this.fromPublicKey = account
this.toPublicKey = entity.toPublicKey
this.content = entity.content
this.chachaKey = if (entity.chachaKey.startsWith("sync:")) "" else entity.chachaKey
this.aesChachaKey = aesChachaKeyValue
this.timestamp = entity.timestamp
this.privateKey = CryptoManager.generatePrivateKeyHash(privateKey)
this.messageId = entity.messageId
this.attachments = emptyList() // Attachments already uploaded, tags are in content
}
ProtocolManager.send(packet)
android.util.Log.d("MessageRepository", "🔄 Resent WAITING message: ${entity.messageId.take(8)}")
} catch (e: Exception) {
android.util.Log.e("MessageRepository", "❌ Failed to retry message ${entity.messageId.take(8)}: ${e.message}")
// Mark as ERROR if retry fails
messageDao.updateDeliveryStatus(account, entity.messageId, DeliveryStatus.ERROR.value)
val dialogKey = getDialogKey(entity.toPublicKey)
updateMessageStatus(dialogKey, entity.messageId, DeliveryStatus.ERROR)
}
}
}
// ===============================
// Private helpers
// ===============================
@@ -1100,6 +1197,53 @@ class MessageRepository private constructor(private val context: Context) {
messageCache.remove(dialogKey)
}
/**
* Desktop parity: clear the one-shot guard so that names can be re-requested
* after reconnect / sync. Desktop's useUserInformation re-fires on every render;
* on Android we clear the guard after each sync cycle instead.
*/
fun clearUserInfoRequestCache() {
requestedUserInfoKeys.clear()
}
/**
* Desktop parity: after sync, resolve names for ALL dialogs that still have
* an empty / placeholder title. Desktop does this per-component via useUserInformation;
* we batch it here for efficiency.
*/
suspend fun requestMissingUserInfo() {
val account = currentAccount ?: return
val privateKey = currentPrivateKey ?: return
val privateKeyHash = CryptoManager.generatePrivateKeyHash(privateKey)
// Query dialogs with empty or placeholder titles
val dialogs = dialogDao.getDialogsWithEmptyTitle(account)
for (dialog in dialogs) {
// Skip self (Saved Messages)
if (dialog.opponentKey == account) continue
// Skip if already requested in this cycle
if (requestedUserInfoKeys.contains(dialog.opponentKey)) continue
requestedUserInfoKeys.add(dialog.opponentKey)
val packet = PacketSearch().apply {
this.privateKey = privateKeyHash
this.search = dialog.opponentKey
}
ProtocolManager.send(packet)
// Small delay to avoid flooding the server with search requests
kotlinx.coroutines.delay(50)
}
}
/**
* Force-request user info, bypassing the one-shot guard.
* Use when opening a dialog to ensure the name/username is fresh.
*/
fun forceRequestUserInfo(publicKey: String) {
requestedUserInfoKeys.remove(publicKey)
requestUserInfo(publicKey)
}
/**
* Запросить информацию о пользователе с сервера 🔥 Защита от бесконечных запросов - каждый ключ
* запрашивается только один раз

View File

@@ -17,15 +17,28 @@ object ReleaseNotes {
val RELEASE_NOTICE = """
Update v$VERSION_PLACEHOLDER
- Исправлена критическая ошибка синхронизации сообщений между ПК и мобильным устройством
- Подтверждение доставки теперь отправляется только после успешной обработки
- Автоматическая синхронизация при возврате из фона
- Анимация сайдбара в стиле Telegram
- Исправлены артефакты на разделителях при анимации
- Улучшено качество блюра аватара на экранах профиля
- Устранены артефакты по краям изображения
- Обновлён цвет шапки и сайдбара в светлой теме
- Белая галочка верификации на экранах профиля
Синхронизация
- Исправлена рассинхронизация сообщений между ПК и Android
- Зависшие сообщения автоматически переотправляются после реконнекта
- Уведомления больше не дублируются во время синхронизации
- Время доставки сообщений теперь корректно обновляется
Контакты
- Имена и юзернеймы загружаются автоматически при первом запуске
- Имя контакта обновляется при открытии чата и входящем сообщении
Соединение
- Исправлен баг с зависанием WebSocket-соединения
- Автореконнект теперь срабатывает корректно при разрыве связи
Обновления
- Система автообновлений — проверка, загрузка и установка APK
- Новый экран обновлений с детальной информацией
Интерфейс
- Бейдж непрочитанных на стрелке назад в чате
- Новый значок верификации
- Печатает = всегда онлайн
""".trimIndent()
fun getNotice(version: String): String =