feat: Bump version to 1.0.9 and update release notes; remove debug logs functionality
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 на кнопке "назад" в экране чата
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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() }
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user