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
|
// Rosetta versioning — bump here on each release
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
val rosettaVersionName = "1.0.8"
|
val rosettaVersionName = "1.0.9"
|
||||||
val rosettaVersionCode = 8 // Increment on each release
|
val rosettaVersionCode = 9 // Increment on each release
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.rosetta.messenger"
|
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.auth.DeviceConfirmScreen
|
||||||
import com.rosetta.messenger.ui.chats.ChatDetailScreen
|
import com.rosetta.messenger.ui.chats.ChatDetailScreen
|
||||||
import com.rosetta.messenger.ui.chats.ChatsListScreen
|
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.RequestsListScreen
|
||||||
import com.rosetta.messenger.ui.chats.SearchScreen
|
import com.rosetta.messenger.ui.chats.SearchScreen
|
||||||
import com.rosetta.messenger.ui.components.OptimizedEmojiCache
|
import com.rosetta.messenger.ui.components.OptimizedEmojiCache
|
||||||
@@ -507,7 +506,6 @@ sealed class Screen {
|
|||||||
data object CrashLogs : Screen()
|
data object CrashLogs : Screen()
|
||||||
data object Biometric : Screen()
|
data object Biometric : Screen()
|
||||||
data object Appearance : Screen()
|
data object Appearance : Screen()
|
||||||
data object DebugLogs : Screen()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -616,9 +614,6 @@ fun MainScreen(
|
|||||||
val isAppearanceVisible by remember {
|
val isAppearanceVisible by remember {
|
||||||
derivedStateOf { navStack.any { it is Screen.Appearance } }
|
derivedStateOf { navStack.any { it is Screen.Appearance } }
|
||||||
}
|
}
|
||||||
val isDebugLogsVisible by remember {
|
|
||||||
derivedStateOf { navStack.any { it is Screen.DebugLogs } }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigation helpers
|
// Navigation helpers
|
||||||
fun pushScreen(screen: Screen) {
|
fun pushScreen(screen: Screen) {
|
||||||
@@ -640,8 +635,7 @@ fun MainScreen(
|
|||||||
it is Screen.Logs ||
|
it is Screen.Logs ||
|
||||||
it is Screen.CrashLogs ||
|
it is Screen.CrashLogs ||
|
||||||
it is Screen.Biometric ||
|
it is Screen.Biometric ||
|
||||||
it is Screen.Appearance ||
|
it is Screen.Appearance
|
||||||
it is Screen.DebugLogs
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun popChatAndChildren() {
|
fun popChatAndChildren() {
|
||||||
@@ -718,7 +712,6 @@ fun MainScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
onSettingsClick = { pushScreen(Screen.Profile) },
|
onSettingsClick = { pushScreen(Screen.Profile) },
|
||||||
onLogsClick = { pushScreen(Screen.DebugLogs) },
|
|
||||||
onInviteFriendsClick = {
|
onInviteFriendsClick = {
|
||||||
// TODO: Share invite link
|
// 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) }
|
var isOtherProfileSwipeEnabled by remember { mutableStateOf(true) }
|
||||||
LaunchedEffect(selectedOtherUser?.publicKey) {
|
LaunchedEffect(selectedOtherUser?.publicKey) {
|
||||||
isOtherProfileSwipeEnabled = true
|
isOtherProfileSwipeEnabled = true
|
||||||
|
|||||||
@@ -17,22 +17,27 @@ object ReleaseNotes {
|
|||||||
val RELEASE_NOTICE = """
|
val RELEASE_NOTICE = """
|
||||||
Update v$VERSION_PLACEHOLDER
|
Update v$VERSION_PLACEHOLDER
|
||||||
|
|
||||||
Загрузка файлов и фото
|
Стабильность и производительность
|
||||||
- Исправлена ошибка загрузки больших файлов (>2 МБ) — стриминг вместо загрузки в память
|
- Фильтрация неподдерживаемых пакетов (групповые чаты) — исключение крэшей при обработке
|
||||||
- Автоматический повторный запрос при сбое скачивания фото
|
- Таймаут очереди входящих сообщений (20 сек) — защита от зависания синхронизации
|
||||||
- Проверка целостности загруженных данных
|
- Повторный запрос синхронизации при таймауте без потери сообщений
|
||||||
- Исправлены OOM-крэши при расшифровке тяжёлых вложений
|
- Отключено накопление отладочных логов в памяти — снижение расхода RAM
|
||||||
|
|
||||||
Фото и медиа
|
Индикаторы прочтения
|
||||||
- Исправлено открытие неправильного фото по нажатию на реплай
|
- Исправлена логика отображения статуса «прочитано» в чате
|
||||||
- Корректный показ фото из пересланных сообщений
|
- Добавлена повторная отправка read receipt при сбое
|
||||||
|
- Автоматическая отправка read receipt при обновлении сообщений из БД
|
||||||
|
|
||||||
|
Верификация
|
||||||
|
- Бейдж верификации корректно сохраняется при обновлении имени собеседника
|
||||||
|
- Статус верификации передаётся при открытии диалога
|
||||||
|
|
||||||
|
FCM Push-уведомления
|
||||||
|
- Дедупликация подписки FCM-токена — устранены повторные регистрации
|
||||||
|
- Автоматическая отписка старого токена перед регистрацией нового
|
||||||
|
|
||||||
Интерфейс
|
Интерфейс
|
||||||
- Бейдж непрочитанных больше не перекрывает стрелку назад
|
- Убран отладочный интерфейс (Debug Logs) из бокового меню и экрана чата
|
||||||
- Индикатор печати адаптирован под светлую тему
|
|
||||||
- Белый значок верификации в профиле собеседника
|
|
||||||
- Белые галочки на пузырьках с файлами
|
|
||||||
- Убран отладочный интерфейс из боковой панели
|
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
fun getNotice(version: String): String =
|
fun getNotice(version: String): String =
|
||||||
|
|||||||
@@ -643,6 +643,22 @@ interface DialogDao {
|
|||||||
verified: Int
|
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 на кнопке "назад" в экране чата
|
* отображения badge на кнопке "назад" в экране чата
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
@@ -22,7 +21,7 @@ import kotlin.coroutines.resume
|
|||||||
*/
|
*/
|
||||||
object ProtocolManager {
|
object ProtocolManager {
|
||||||
private const val TAG = "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
|
// Server address - same as React Native version
|
||||||
private const val SERVER_ADDRESS = "ws://46.28.71.12:3000"
|
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
|
// Pending resolves: publicKey → list of continuations waiting for the result
|
||||||
private val pendingResolves = ConcurrentHashMap<String, MutableList<kotlinx.coroutines.CancellableContinuation<SearchUser?>>>()
|
private val pendingResolves = ConcurrentHashMap<String, MutableList<kotlinx.coroutines.CancellableContinuation<SearchUser?>>>()
|
||||||
|
|
||||||
private val dateFormat = SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault())
|
|
||||||
|
|
||||||
// 🚀 Флаг для включения UI логов (по умолчанию ВЫКЛЮЧЕНО - это вызывало ANR!)
|
// 🚀 Флаг для включения UI логов (по умолчанию ВЫКЛЮЧЕНО - это вызывало ANR!)
|
||||||
private var uiLogsEnabled = false
|
private var uiLogsEnabled = false
|
||||||
private var lastProtocolState: ProtocolState? = null
|
private var lastProtocolState: ProtocolState? = null
|
||||||
@@ -91,12 +88,9 @@ object ProtocolManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addLog(message: String) {
|
fun addLog(@Suppress("UNUSED_PARAMETER") message: String) {
|
||||||
val timestamp = dateFormat.format(Date())
|
// Disabled by request: keep debug log buffer empty.
|
||||||
val logLine = "[$timestamp] $message"
|
return
|
||||||
|
|
||||||
// Always keep logs in memory for the Logs screen (opened via `...` in chat)
|
|
||||||
_debugLogs.value = (_debugLogs.value + logLine).takeLast(MAX_DEBUG_LOGS)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun enableUILogs(enabled: Boolean) {
|
fun enableUILogs(enabled: Boolean) {
|
||||||
@@ -169,6 +163,18 @@ object ProtocolManager {
|
|||||||
requireResyncAfterAccountInit("⏳ Incoming message before account init, scheduling re-sync")
|
requireResyncAfterAccountInit("⏳ Incoming message before account init, scheduling re-sync")
|
||||||
return@launchInboundPacketTask
|
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 {
|
try {
|
||||||
repository.handleIncomingMessage(messagePacket)
|
repository.handleIncomingMessage(messagePacket)
|
||||||
if (!syncBatchInProgress) {
|
if (!syncBatchInProgress) {
|
||||||
@@ -176,7 +182,6 @@ object ProtocolManager {
|
|||||||
}
|
}
|
||||||
// ✅ Send delivery ACK only AFTER message is safely stored in DB.
|
// ✅ Send delivery ACK only AFTER message is safely stored in DB.
|
||||||
// Skip for own sync messages (no need to ACK yourself).
|
// Skip for own sync messages (no need to ACK yourself).
|
||||||
val ownKey = getProtocol().getPublicKey()
|
|
||||||
if (messagePacket.fromPublicKey != ownKey) {
|
if (messagePacket.fromPublicKey != ownKey) {
|
||||||
val deliveryPacket = PacketDelivery().apply {
|
val deliveryPacket = PacketDelivery().apply {
|
||||||
messageId = messagePacket.messageId
|
messageId = messagePacket.messageId
|
||||||
@@ -219,6 +224,18 @@ object ProtocolManager {
|
|||||||
requireResyncAfterAccountInit("⏳ Read status before account init, scheduling re-sync")
|
requireResyncAfterAccountInit("⏳ Read status before account init, scheduling re-sync")
|
||||||
return@launchInboundPacketTask
|
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)
|
repository.handleRead(readPacket)
|
||||||
if (!syncBatchInProgress) {
|
if (!syncBatchInProgress) {
|
||||||
repository.updateLastSyncTimestamp(System.currentTimeMillis())
|
repository.updateLastSyncTimestamp(System.currentTimeMillis())
|
||||||
@@ -365,9 +382,14 @@ object ProtocolManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchInboundPacketTask(block: suspend () -> Unit) {
|
private fun launchInboundPacketTask(block: suspend () -> Unit): Boolean {
|
||||||
ensureInboundQueueDrainRunning()
|
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) {
|
private fun requireResyncAfterAccountInit(reason: String) {
|
||||||
@@ -383,10 +405,53 @@ object ProtocolManager {
|
|||||||
* Since the queue is strictly FIFO, when the sentinel runs, all previously
|
* Since the queue is strictly FIFO, when the sentinel runs, all previously
|
||||||
* submitted tasks are guaranteed to have completed.
|
* 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>()
|
val done = CompletableDeferred<Unit>()
|
||||||
launchInboundPacketTask { done.complete(Unit) }
|
if (!launchInboundPacketTask { done.complete(Unit) }) {
|
||||||
done.await()
|
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() {
|
private fun onAuthenticated() {
|
||||||
@@ -499,10 +564,21 @@ object ProtocolManager {
|
|||||||
// syncBatchInProgress stays true until NOT_NEEDED arrives.
|
// syncBatchInProgress stays true until NOT_NEEDED arrives.
|
||||||
scope.launch {
|
scope.launch {
|
||||||
setSyncInProgress(true)
|
setSyncInProgress(true)
|
||||||
// Desktop: await whenFinish() — wait for ALL queued tasks without timeout.
|
val tasksFinished = whenInboundTasksFinish()
|
||||||
// Old code used 15s polling timeout which could advance the sync timestamp
|
if (!tasksFinished) {
|
||||||
// before all messages were processed, causing message loss on app crash.
|
android.util.Log.w(
|
||||||
whenInboundTasksFinish()
|
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")
|
addLog("🔄 SYNC tasks done — saving timestamp ${packet.timestamp}, requesting next batch")
|
||||||
messageRepository?.updateLastSyncTimestamp(packet.timestamp)
|
messageRepository?.updateLastSyncTimestamp(packet.timestamp)
|
||||||
sendSynchronize(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.data.MessageRepository
|
||||||
import com.rosetta.messenger.database.RosettaDatabase
|
import com.rosetta.messenger.database.RosettaDatabase
|
||||||
import com.rosetta.messenger.network.AttachmentType
|
import com.rosetta.messenger.network.AttachmentType
|
||||||
import com.rosetta.messenger.network.ProtocolManager
|
|
||||||
import com.rosetta.messenger.network.SearchUser
|
import com.rosetta.messenger.network.SearchUser
|
||||||
import com.rosetta.messenger.repository.AvatarRepository
|
import com.rosetta.messenger.repository.AvatarRepository
|
||||||
import com.rosetta.messenger.ui.chats.attach.ChatAttachAlert
|
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 showMenu by remember { mutableStateOf(false) }
|
||||||
var showDebugLogs by remember { mutableStateOf(false) }
|
|
||||||
var showDeleteConfirm by remember { mutableStateOf(false) }
|
var showDeleteConfirm by remember { mutableStateOf(false) }
|
||||||
var showBlockConfirm by remember { mutableStateOf(false) }
|
var showBlockConfirm by remember { mutableStateOf(false) }
|
||||||
var showUnblockConfirm by remember { mutableStateOf(false) }
|
var showUnblockConfirm by remember { mutableStateOf(false) }
|
||||||
val debugLogs by ProtocolManager.debugLogs.collectAsState()
|
|
||||||
// Наблюдаем за статусом блокировки в реальном времени через Flow
|
// Наблюдаем за статусом блокировки в реальном времени через Flow
|
||||||
val isBlocked by
|
val isBlocked by
|
||||||
database.blacklistDao()
|
database.blacklistDao()
|
||||||
@@ -606,7 +598,7 @@ fun ChatDetailScreen(
|
|||||||
// forwardTrigger добавлен чтобы перезагрузить диалог при forward в тот же чат
|
// forwardTrigger добавлен чтобы перезагрузить диалог при forward в тот же чат
|
||||||
LaunchedEffect(user.publicKey, forwardTrigger) {
|
LaunchedEffect(user.publicKey, forwardTrigger) {
|
||||||
viewModel.setUserKeys(currentUserPublicKey, currentUserPrivateKey)
|
viewModel.setUserKeys(currentUserPublicKey, currentUserPrivateKey)
|
||||||
viewModel.openDialog(user.publicKey, user.title, user.username)
|
viewModel.openDialog(user.publicKey, user.title, user.username, user.verified)
|
||||||
viewModel.markVisibleMessagesAsRead()
|
viewModel.markVisibleMessagesAsRead()
|
||||||
// 🔥 Убираем уведомление этого чата из шторки при заходе
|
// 🔥 Убираем уведомление этого чата из шторки при заходе
|
||||||
com.rosetta.messenger.push.RosettaFirebaseMessagingService
|
com.rosetta.messenger.push.RosettaFirebaseMessagingService
|
||||||
@@ -1221,10 +1213,6 @@ fun ChatDetailScreen(
|
|||||||
isSystemAccount,
|
isSystemAccount,
|
||||||
isBlocked =
|
isBlocked =
|
||||||
isBlocked,
|
isBlocked,
|
||||||
onLogsClick = {
|
|
||||||
showMenu = false
|
|
||||||
showDebugLogs = true
|
|
||||||
},
|
|
||||||
onBlockClick = {
|
onBlockClick = {
|
||||||
showMenu =
|
showMenu =
|
||||||
false
|
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 opponentTitle: String = ""
|
||||||
private var opponentUsername: String = ""
|
private var opponentUsername: String = ""
|
||||||
|
private var opponentVerified: Int = 0
|
||||||
|
|
||||||
// Текущий диалог
|
// Текущий диалог
|
||||||
private var opponentKey: String? = null
|
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) {
|
// if (opponentKey == publicKey) {
|
||||||
@@ -547,6 +548,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
opponentKey = publicKey
|
opponentKey = publicKey
|
||||||
opponentTitle = title
|
opponentTitle = title
|
||||||
opponentUsername = username
|
opponentUsername = username
|
||||||
|
opponentVerified = verified.coerceAtLeast(0)
|
||||||
|
|
||||||
// 📨 СНАЧАЛА проверяем ForwardManager - ДО сброса состояния!
|
// 📨 СНАЧАЛА проверяем ForwardManager - ДО сброса состояния!
|
||||||
// Это важно для правильного отображения forward сообщений сразу
|
// Это важно для правильного отображения forward сообщений сразу
|
||||||
@@ -1774,7 +1776,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
title = opponentTitle,
|
title = opponentTitle,
|
||||||
username = opponentUsername,
|
username = opponentUsername,
|
||||||
publicKey = publicKey,
|
publicKey = publicKey,
|
||||||
verified = 0,
|
verified = opponentVerified,
|
||||||
online = 0
|
online = 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1791,7 +1793,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
title = dialog.opponentTitle,
|
title = dialog.opponentTitle,
|
||||||
username = dialog.opponentUsername,
|
username = dialog.opponentUsername,
|
||||||
publicKey = publicKey,
|
publicKey = publicKey,
|
||||||
verified = 0,
|
verified = dialog.verified,
|
||||||
online = 0
|
online = 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -3569,8 +3571,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
// 🔥 FIX: updateDialogFromMessages создаёт новый диалог с пустым title/username
|
// 🔥 FIX: updateDialogFromMessages создаёт новый диалог с пустым title/username
|
||||||
// когда диалога ещё не было. Обновляем метаданные из уже известных данных.
|
// когда диалога ещё не было. Обновляем метаданные из уже известных данных.
|
||||||
if (opponent != account && opponentTitle.isNotEmpty()) {
|
if (opponent != account && opponentTitle.isNotEmpty()) {
|
||||||
dialogDao.updateOpponentInfo(
|
dialogDao.updateOpponentDisplayName(
|
||||||
account, opponent, opponentTitle, opponentUsername, 0
|
account, opponent, opponentTitle, opponentUsername
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {}
|
} catch (e: Exception) {}
|
||||||
@@ -3602,8 +3604,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
// 🔥 FIX: Сохраняем title/username после пересчёта счётчиков
|
// 🔥 FIX: Сохраняем title/username после пересчёта счётчиков
|
||||||
if (opponentKey != account && opponentTitle.isNotEmpty()) {
|
if (opponentKey != account && opponentTitle.isNotEmpty()) {
|
||||||
dialogDao.updateOpponentInfo(
|
dialogDao.updateOpponentDisplayName(
|
||||||
account, opponentKey, opponentTitle, opponentUsername, 0
|
account, opponentKey, opponentTitle, opponentUsername
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {}
|
} catch (e: Exception) {}
|
||||||
|
|||||||
@@ -235,8 +235,7 @@ fun ChatsListScreen(
|
|||||||
avatarRepository: com.rosetta.messenger.repository.AvatarRepository? = null,
|
avatarRepository: com.rosetta.messenger.repository.AvatarRepository? = null,
|
||||||
onAddAccount: () -> Unit = {},
|
onAddAccount: () -> Unit = {},
|
||||||
onSwitchAccount: (String) -> Unit = {},
|
onSwitchAccount: (String) -> Unit = {},
|
||||||
onDeleteAccountFromSidebar: (String) -> Unit = {},
|
onDeleteAccountFromSidebar: (String) -> Unit = {}
|
||||||
onLogsClick: () -> Unit = {}
|
|
||||||
) {
|
) {
|
||||||
// Theme transition state
|
// Theme transition state
|
||||||
var hasInitialized by remember { mutableStateOf(false) }
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
object AttachmentDownloadDebugLogger {
|
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())
|
private val _logs = MutableStateFlow<List<String>>(emptyList())
|
||||||
val logs: StateFlow<List<String>> = _logs.asStateFlow()
|
val logs: StateFlow<List<String>> = _logs.asStateFlow()
|
||||||
|
|
||||||
fun log(message: String) {
|
fun log(@Suppress("UNUSED_PARAMETER") message: String) {
|
||||||
val timestamp = dateFormat.format(Date())
|
// Disabled by request: no runtime accumulation of photo debug logs.
|
||||||
val line = "[$timestamp] 🖼️ $message"
|
return
|
||||||
_logs.update { current -> (current + line).takeLast(MAX_LOGS) }
|
|
||||||
// Всегда дублируем в debug logs чата напрямую через ProtocolManager
|
|
||||||
// (не через MessageLogger, чтобы обойти isEnabled гейт)
|
|
||||||
com.rosetta.messenger.network.ProtocolManager.addLog("🖼️ $message")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
|
|||||||
@@ -2370,7 +2370,6 @@ fun KebabMenu(
|
|||||||
isSavedMessages: Boolean,
|
isSavedMessages: Boolean,
|
||||||
isSystemAccount: Boolean = false,
|
isSystemAccount: Boolean = false,
|
||||||
isBlocked: Boolean,
|
isBlocked: Boolean,
|
||||||
onLogsClick: () -> Unit,
|
|
||||||
onBlockClick: () -> Unit,
|
onBlockClick: () -> Unit,
|
||||||
onUnblockClick: () -> Unit,
|
onUnblockClick: () -> Unit,
|
||||||
onDeleteClick: () -> Unit
|
onDeleteClick: () -> Unit
|
||||||
@@ -2399,14 +2398,6 @@ fun KebabMenu(
|
|||||||
dismissOnClickOutside = true
|
dismissOnClickOutside = true
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
KebabMenuItem(
|
|
||||||
icon = TelegramIcons.Info,
|
|
||||||
text = "Debug Logs",
|
|
||||||
onClick = onLogsClick,
|
|
||||||
tintColor = iconColor,
|
|
||||||
textColor = textColor
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!isSavedMessages && !isSystemAccount) {
|
if (!isSavedMessages && !isSystemAccount) {
|
||||||
KebabMenuItem(
|
KebabMenuItem(
|
||||||
icon = if (isBlocked) TelegramIcons.Done else TelegramIcons.Block,
|
icon = if (isBlocked) TelegramIcons.Done else TelegramIcons.Block,
|
||||||
|
|||||||
Reference in New Issue
Block a user