feat: Bump version to 1.0.7, enhance message delivery handling, and add connection logs screen
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Запросить информацию о пользователе с сервера 🔥 Защита от бесконечных запросов - каждый ключ
|
||||
* запрашивается только один раз
|
||||
|
||||
@@ -17,15 +17,28 @@ object ReleaseNotes {
|
||||
val RELEASE_NOTICE = """
|
||||
Update v$VERSION_PLACEHOLDER
|
||||
|
||||
- Исправлена критическая ошибка синхронизации сообщений между ПК и мобильным устройством
|
||||
- Подтверждение доставки теперь отправляется только после успешной обработки
|
||||
- Автоматическая синхронизация при возврате из фона
|
||||
- Анимация сайдбара в стиле Telegram
|
||||
- Исправлены артефакты на разделителях при анимации
|
||||
- Улучшено качество блюра аватара на экранах профиля
|
||||
- Устранены артефакты по краям изображения
|
||||
- Обновлён цвет шапки и сайдбара в светлой теме
|
||||
- Белая галочка верификации на экранах профиля
|
||||
Синхронизация
|
||||
- Исправлена рассинхронизация сообщений между ПК и Android
|
||||
- Зависшие сообщения автоматически переотправляются после реконнекта
|
||||
- Уведомления больше не дублируются во время синхронизации
|
||||
- Время доставки сообщений теперь корректно обновляется
|
||||
|
||||
Контакты
|
||||
- Имена и юзернеймы загружаются автоматически при первом запуске
|
||||
- Имя контакта обновляется при открытии чата и входящем сообщении
|
||||
|
||||
Соединение
|
||||
- Исправлен баг с зависанием WebSocket-соединения
|
||||
- Автореконнект теперь срабатывает корректно при разрыве связи
|
||||
|
||||
Обновления
|
||||
- Система автообновлений — проверка, загрузка и установка APK
|
||||
- Новый экран обновлений с детальной информацией
|
||||
|
||||
Интерфейс
|
||||
- Бейдж непрочитанных на стрелке назад в чате
|
||||
- Новый значок верификации
|
||||
- Печатает = всегда онлайн
|
||||
""".trimIndent()
|
||||
|
||||
fun getNotice(version: String): String =
|
||||
|
||||
Reference in New Issue
Block a user