feat: Bump version to 1.0.9 and update release notes; remove debug logs functionality

This commit is contained in:
2026-02-26 16:02:01 +05:00
parent f526a442b0
commit 388d279ea9
10 changed files with 147 additions and 125 deletions

View File

@@ -23,8 +23,8 @@ val gitShortSha = safeGitOutput("rev-parse", "--short", "HEAD") ?: "unknown"
// ═══════════════════════════════════════════════════════════
// Rosetta versioning — bump here on each release
// ═══════════════════════════════════════════════════════════
val rosettaVersionName = "1.0.8"
val rosettaVersionCode = 8 // Increment on each release
val rosettaVersionName = "1.0.9"
val rosettaVersionCode = 9 // Increment on each release
android {
namespace = "com.rosetta.messenger"

View File

@@ -41,7 +41,6 @@ import com.rosetta.messenger.ui.auth.AuthFlow
import com.rosetta.messenger.ui.auth.DeviceConfirmScreen
import com.rosetta.messenger.ui.chats.ChatDetailScreen
import com.rosetta.messenger.ui.chats.ChatsListScreen
import com.rosetta.messenger.ui.chats.ConnectionLogsScreen
import com.rosetta.messenger.ui.chats.RequestsListScreen
import com.rosetta.messenger.ui.chats.SearchScreen
import com.rosetta.messenger.ui.components.OptimizedEmojiCache
@@ -507,7 +506,6 @@ sealed class Screen {
data object CrashLogs : Screen()
data object Biometric : Screen()
data object Appearance : Screen()
data object DebugLogs : Screen()
}
@Composable
@@ -616,9 +614,6 @@ fun MainScreen(
val isAppearanceVisible by remember {
derivedStateOf { navStack.any { it is Screen.Appearance } }
}
val isDebugLogsVisible by remember {
derivedStateOf { navStack.any { it is Screen.DebugLogs } }
}
// Navigation helpers
fun pushScreen(screen: Screen) {
@@ -640,8 +635,7 @@ fun MainScreen(
it is Screen.Logs ||
it is Screen.CrashLogs ||
it is Screen.Biometric ||
it is Screen.Appearance ||
it is Screen.DebugLogs
it is Screen.Appearance
}
}
fun popChatAndChildren() {
@@ -718,7 +712,6 @@ fun MainScreen(
)
},
onSettingsClick = { pushScreen(Screen.Profile) },
onLogsClick = { pushScreen(Screen.DebugLogs) },
onInviteFriendsClick = {
// TODO: Share invite link
},
@@ -1010,17 +1003,6 @@ fun MainScreen(
)
}
SwipeBackContainer(
isVisible = isDebugLogsVisible,
onBack = { navStack = navStack.filterNot { it is Screen.DebugLogs } },
isDarkTheme = isDarkTheme
) {
ConnectionLogsScreen(
isDarkTheme = isDarkTheme,
onBack = { navStack = navStack.filterNot { it is Screen.DebugLogs } }
)
}
var isOtherProfileSwipeEnabled by remember { mutableStateOf(true) }
LaunchedEffect(selectedOtherUser?.publicKey) {
isOtherProfileSwipeEnabled = true

View File

@@ -17,22 +17,27 @@ object ReleaseNotes {
val RELEASE_NOTICE = """
Update v$VERSION_PLACEHOLDER
Загрузка файлов и фото
- Исправлена ошибка загрузки больших файлов (>2 МБ) — стриминг вместо загрузки в память
- Автоматический повторный запрос при сбое скачивания фото
- Проверка целостности загруженных данных
- Исправлены OOM-крэши при расшифровке тяжёлых вложений
Стабильность и производительность
- Фильтрация неподдерживаемых пакетов (групповые чаты) — исключение крэшей при обработке
- Таймаут очереди входящих сообщений (20 сек) — защита от зависания синхронизации
- Повторный запрос синхронизации при таймауте без потери сообщений
- Отключено накопление отладочных логов в памяти — снижение расхода RAM
Фото и медиа
- Исправлено открытие неправильного фото по нажатию на реплай
- Корректный показ фото из пересланных сообщений
Индикаторы прочтения
- Исправлена логика отображения статуса «прочитано» в чате
- Добавлена повторная отправка read receipt при сбое
- Автоматическая отправка read receipt при обновлении сообщений из БД
Верификация
- Бейдж верификации корректно сохраняется при обновлении имени собеседника
- Статус верификации передаётся при открытии диалога
FCM Push-уведомления
- Дедупликация подписки FCM-токена — устранены повторные регистрации
- Автоматическая отписка старого токена перед регистрацией нового
Интерфейс
- Бейдж непрочитанных больше не перекрывает стрелку назад
- Индикатор печати адаптирован под светлую тему
- Белый значок верификации в профиле собеседника
- Белые галочки на пузырьках с файлами
- Убран отладочный интерфейс из боковой панели
- Убран отладочный интерфейс (Debug Logs) из бокового меню и экрана чата
""".trimIndent()
fun getNotice(version: String): String =

View File

@@ -643,6 +643,22 @@ interface DialogDao {
verified: Int
)
/** Обновить только имя/username собеседника, не трогая verified */
@Query(
"""
UPDATE dialogs SET
opponent_title = :title,
opponent_username = :username
WHERE account = :account AND opponent_key = :opponentKey
"""
)
suspend fun updateOpponentDisplayName(
account: String,
opponentKey: String,
title: String,
username: String
)
/**
* Получить общее количество непрочитанных сообщений, исключая указанный диалог Используется для
* отображения badge на кнопке "назад" в экране чата

View File

@@ -11,7 +11,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import java.security.SecureRandom
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import kotlin.coroutines.resume
@@ -22,7 +21,7 @@ import kotlin.coroutines.resume
*/
object ProtocolManager {
private const val TAG = "ProtocolManager"
private const val MAX_DEBUG_LOGS = 2000
private const val INBOUND_QUEUE_WAIT_TIMEOUT_MS = 20_000L
// Server address - same as React Native version
private const val SERVER_ADDRESS = "ws://46.28.71.12:3000"
@@ -65,8 +64,6 @@ object ProtocolManager {
// Pending resolves: publicKey → list of continuations waiting for the result
private val pendingResolves = ConcurrentHashMap<String, MutableList<kotlinx.coroutines.CancellableContinuation<SearchUser?>>>()
private val dateFormat = SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault())
// 🚀 Флаг для включения UI логов (по умолчанию ВЫКЛЮЧЕНО - это вызывало ANR!)
private var uiLogsEnabled = false
private var lastProtocolState: ProtocolState? = null
@@ -91,12 +88,9 @@ object ProtocolManager {
}
}
fun addLog(message: String) {
val timestamp = dateFormat.format(Date())
val logLine = "[$timestamp] $message"
// Always keep logs in memory for the Logs screen (opened via `...` in chat)
_debugLogs.value = (_debugLogs.value + logLine).takeLast(MAX_DEBUG_LOGS)
fun addLog(@Suppress("UNUSED_PARAMETER") message: String) {
// Disabled by request: keep debug log buffer empty.
return
}
fun enableUILogs(enabled: Boolean) {
@@ -169,6 +163,18 @@ object ProtocolManager {
requireResyncAfterAccountInit("⏳ Incoming message before account init, scheduling re-sync")
return@launchInboundPacketTask
}
val ownKey = getProtocol().getPublicKey().orEmpty()
if (ownKey.isBlank()) {
requireResyncAfterAccountInit("⏳ Incoming message before protocol account init, scheduling re-sync")
return@launchInboundPacketTask
}
if (!isSupportedDirectMessagePacket(messagePacket, ownKey)) {
android.util.Log.w(
TAG,
"Skipping unsupported message packet (likely conversation): from=${messagePacket.fromPublicKey.take(16)}, to=${messagePacket.toPublicKey.take(16)}"
)
return@launchInboundPacketTask
}
try {
repository.handleIncomingMessage(messagePacket)
if (!syncBatchInProgress) {
@@ -176,7 +182,6 @@ object ProtocolManager {
}
// ✅ Send delivery ACK only AFTER message is safely stored in DB.
// Skip for own sync messages (no need to ACK yourself).
val ownKey = getProtocol().getPublicKey()
if (messagePacket.fromPublicKey != ownKey) {
val deliveryPacket = PacketDelivery().apply {
messageId = messagePacket.messageId
@@ -219,6 +224,18 @@ object ProtocolManager {
requireResyncAfterAccountInit("⏳ Read status before account init, scheduling re-sync")
return@launchInboundPacketTask
}
val ownKey = getProtocol().getPublicKey().orEmpty()
if (ownKey.isBlank()) {
requireResyncAfterAccountInit("⏳ Read status before protocol account init, scheduling re-sync")
return@launchInboundPacketTask
}
if (!isSupportedDirectReadPacket(readPacket, ownKey)) {
android.util.Log.w(
TAG,
"Skipping unsupported read packet (likely conversation): from=${readPacket.fromPublicKey.take(16)}, to=${readPacket.toPublicKey.take(16)}"
)
return@launchInboundPacketTask
}
repository.handleRead(readPacket)
if (!syncBatchInProgress) {
repository.updateLastSyncTimestamp(System.currentTimeMillis())
@@ -365,9 +382,14 @@ object ProtocolManager {
}
}
private fun launchInboundPacketTask(block: suspend () -> Unit) {
private fun launchInboundPacketTask(block: suspend () -> Unit): Boolean {
ensureInboundQueueDrainRunning()
inboundTaskChannel.trySend(block)
val result = inboundTaskChannel.trySend(block)
if (result.isFailure) {
android.util.Log.e(TAG, "Failed to enqueue inbound task", result.exceptionOrNull())
return false
}
return true
}
private fun requireResyncAfterAccountInit(reason: String) {
@@ -383,10 +405,53 @@ object ProtocolManager {
* Since the queue is strictly FIFO, when the sentinel runs, all previously
* submitted tasks are guaranteed to have completed.
*/
private suspend fun whenInboundTasksFinish() {
private suspend fun whenInboundTasksFinish(timeoutMs: Long = INBOUND_QUEUE_WAIT_TIMEOUT_MS): Boolean {
val done = CompletableDeferred<Unit>()
launchInboundPacketTask { done.complete(Unit) }
done.await()
if (!launchInboundPacketTask { done.complete(Unit) }) {
return false
}
return try {
withTimeout(timeoutMs) { done.await() }
true
} catch (_: TimeoutCancellationException) {
false
}
}
private fun isLikelyUserPublicKey(value: String): Boolean {
val normalized = value.removePrefix("0x")
if (normalized.length != 64 && normalized.length != 66 && normalized.length != 128 && normalized.length != 130) {
return false
}
return normalized.all { it.isDigit() || it in 'a'..'f' || it in 'A'..'F' }
}
private fun isSupportedDirectPeerKey(peerKey: String, ownKey: String): Boolean {
return peerKey == ownKey ||
MessageRepository.isSystemAccount(peerKey) ||
isLikelyUserPublicKey(peerKey)
}
private fun isSupportedDirectMessagePacket(packet: PacketMessage, ownKey: String): Boolean {
val from = packet.fromPublicKey.trim()
val to = packet.toPublicKey.trim()
if (from.isBlank() || to.isBlank()) return false
return when {
from == ownKey -> isSupportedDirectPeerKey(to, ownKey)
to == ownKey -> isSupportedDirectPeerKey(from, ownKey)
else -> false
}
}
private fun isSupportedDirectReadPacket(packet: PacketRead, ownKey: String): Boolean {
val from = packet.fromPublicKey.trim()
val to = packet.toPublicKey.trim()
if (from.isBlank() || to.isBlank()) return false
return when {
from == ownKey -> isSupportedDirectPeerKey(to, ownKey)
to == ownKey -> isSupportedDirectPeerKey(from, ownKey)
else -> false
}
}
private fun onAuthenticated() {
@@ -499,10 +564,21 @@ object ProtocolManager {
// syncBatchInProgress stays true until NOT_NEEDED arrives.
scope.launch {
setSyncInProgress(true)
// Desktop: await whenFinish() — wait for ALL queued tasks without timeout.
// Old code used 15s polling timeout which could advance the sync timestamp
// before all messages were processed, causing message loss on app crash.
whenInboundTasksFinish()
val tasksFinished = whenInboundTasksFinish()
if (!tasksFinished) {
android.util.Log.w(
TAG,
"SYNC BATCH_END: inbound queue did not drain in time, requesting re-sync without advancing cursor"
)
val fallbackTimestamp = try {
messageRepository?.getLastSyncTimestamp() ?: packet.timestamp
} catch (e: Exception) {
android.util.Log.e(TAG, "Failed to read last sync timestamp for fallback", e)
packet.timestamp
}
sendSynchronize(fallbackTimestamp)
return@launch
}
addLog("🔄 SYNC tasks done — saving timestamp ${packet.timestamp}, requesting next batch")
messageRepository?.updateLastSyncTimestamp(packet.timestamp)
sendSynchronize(packet.timestamp)

View File

@@ -77,7 +77,6 @@ import com.rosetta.messenger.data.ForwardManager
import com.rosetta.messenger.data.MessageRepository
import com.rosetta.messenger.database.RosettaDatabase
import com.rosetta.messenger.network.AttachmentType
import com.rosetta.messenger.network.ProtocolManager
import com.rosetta.messenger.network.SearchUser
import com.rosetta.messenger.repository.AvatarRepository
import com.rosetta.messenger.ui.chats.attach.ChatAttachAlert
@@ -402,18 +401,11 @@ fun ChatDetailScreen(
}
}
DisposableEffect(Unit) {
ProtocolManager.enableUILogs(true)
onDispose { ProtocolManager.enableUILogs(false) }
}
// Состояние выпадающего меню
var showMenu by remember { mutableStateOf(false) }
var showDebugLogs by remember { mutableStateOf(false) }
var showDeleteConfirm by remember { mutableStateOf(false) }
var showBlockConfirm by remember { mutableStateOf(false) }
var showUnblockConfirm by remember { mutableStateOf(false) }
val debugLogs by ProtocolManager.debugLogs.collectAsState()
// Наблюдаем за статусом блокировки в реальном времени через Flow
val isBlocked by
database.blacklistDao()
@@ -606,7 +598,7 @@ fun ChatDetailScreen(
// forwardTrigger добавлен чтобы перезагрузить диалог при forward в тот же чат
LaunchedEffect(user.publicKey, forwardTrigger) {
viewModel.setUserKeys(currentUserPublicKey, currentUserPrivateKey)
viewModel.openDialog(user.publicKey, user.title, user.username)
viewModel.openDialog(user.publicKey, user.title, user.username, user.verified)
viewModel.markVisibleMessagesAsRead()
// 🔥 Убираем уведомление этого чата из шторки при заходе
com.rosetta.messenger.push.RosettaFirebaseMessagingService
@@ -1221,10 +1213,6 @@ fun ChatDetailScreen(
isSystemAccount,
isBlocked =
isBlocked,
onLogsClick = {
showMenu = false
showDebugLogs = true
},
onBlockClick = {
showMenu =
false
@@ -2745,15 +2733,5 @@ fun ChatDetailScreen(
)
}
if (showDebugLogs) {
DebugLogsBottomSheet(
logs = debugLogs,
isDarkTheme = isDarkTheme,
onDismiss = { showDebugLogs = false },
onClearLogs = { ProtocolManager.clearLogs() }
)
}
}
}

View File

@@ -110,6 +110,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
// Информация о собеседнике
private var opponentTitle: String = ""
private var opponentUsername: String = ""
private var opponentVerified: Int = 0
// Текущий диалог
private var opponentKey: String? = null
@@ -534,7 +535,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
}
/** Открыть диалог */
fun openDialog(publicKey: String, title: String = "", username: String = "") {
fun openDialog(publicKey: String, title: String = "", username: String = "", verified: Int = 0) {
// 🔥 ВСЕГДА перезагружаем данные - не кешируем, т.к. диалог мог быть удалён
// if (opponentKey == publicKey) {
@@ -547,6 +548,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
opponentKey = publicKey
opponentTitle = title
opponentUsername = username
opponentVerified = verified.coerceAtLeast(0)
// 📨 СНАЧАЛА проверяем ForwardManager - ДО сброса состояния!
// Это важно для правильного отображения forward сообщений сразу
@@ -1774,7 +1776,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
title = opponentTitle,
username = opponentUsername,
publicKey = publicKey,
verified = 0,
verified = opponentVerified,
online = 0
)
}
@@ -1791,7 +1793,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
title = dialog.opponentTitle,
username = dialog.opponentUsername,
publicKey = publicKey,
verified = 0,
verified = dialog.verified,
online = 0
)
}
@@ -3569,8 +3571,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
// 🔥 FIX: updateDialogFromMessages создаёт новый диалог с пустым title/username
// когда диалога ещё не было. Обновляем метаданные из уже известных данных.
if (opponent != account && opponentTitle.isNotEmpty()) {
dialogDao.updateOpponentInfo(
account, opponent, opponentTitle, opponentUsername, 0
dialogDao.updateOpponentDisplayName(
account, opponent, opponentTitle, opponentUsername
)
}
} catch (e: Exception) {}
@@ -3602,8 +3604,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
// 🔥 FIX: Сохраняем title/username после пересчёта счётчиков
if (opponentKey != account && opponentTitle.isNotEmpty()) {
dialogDao.updateOpponentInfo(
account, opponentKey, opponentTitle, opponentUsername, 0
dialogDao.updateOpponentDisplayName(
account, opponentKey, opponentTitle, opponentUsername
)
}
} catch (e: Exception) {}

View File

@@ -235,8 +235,7 @@ fun ChatsListScreen(
avatarRepository: com.rosetta.messenger.repository.AvatarRepository? = null,
onAddAccount: () -> Unit = {},
onSwitchAccount: (String) -> Unit = {},
onDeleteAccountFromSidebar: (String) -> Unit = {},
onLogsClick: () -> Unit = {}
onDeleteAccountFromSidebar: (String) -> Unit = {}
) {
// Theme transition state
var hasInitialized by remember { mutableStateOf(false) }
@@ -1149,22 +1148,6 @@ fun ChatsListScreen(
}
)
// 📋 Logs
DrawerMenuItemEnhanced(
icon = TablerIcons.Bug,
text = "Logs",
iconColor = menuIconColor,
textColor = menuTextColor,
onClick = {
scope.launch {
drawerState.close()
kotlinx.coroutines
.delay(100)
onLogsClick()
}
}
)
}
// ═══════════════════════════════════════════════════════════

View File

@@ -3,25 +3,14 @@ package com.rosetta.messenger.ui.chats.components
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
object AttachmentDownloadDebugLogger {
private const val MAX_LOGS = 1000
private val dateFormat = SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault())
private val _logs = MutableStateFlow<List<String>>(emptyList())
val logs: StateFlow<List<String>> = _logs.asStateFlow()
fun log(message: String) {
val timestamp = dateFormat.format(Date())
val line = "[$timestamp] 🖼️ $message"
_logs.update { current -> (current + line).takeLast(MAX_LOGS) }
// Всегда дублируем в debug logs чата напрямую через ProtocolManager
// (не через MessageLogger, чтобы обойти isEnabled гейт)
com.rosetta.messenger.network.ProtocolManager.addLog("🖼️ $message")
fun log(@Suppress("UNUSED_PARAMETER") message: String) {
// Disabled by request: no runtime accumulation of photo debug logs.
return
}
fun clear() {

View File

@@ -2370,7 +2370,6 @@ fun KebabMenu(
isSavedMessages: Boolean,
isSystemAccount: Boolean = false,
isBlocked: Boolean,
onLogsClick: () -> Unit,
onBlockClick: () -> Unit,
onUnblockClick: () -> Unit,
onDeleteClick: () -> Unit
@@ -2399,14 +2398,6 @@ fun KebabMenu(
dismissOnClickOutside = true
)
) {
KebabMenuItem(
icon = TelegramIcons.Info,
text = "Debug Logs",
onClick = onLogsClick,
tintColor = iconColor,
textColor = textColor
)
if (!isSavedMessages && !isSystemAccount) {
KebabMenuItem(
icon = if (isBlocked) TelegramIcons.Done else TelegramIcons.Block,