From 7521b9a11b6b28c25a0432454a6a5644832589e9 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Fri, 17 Apr 2026 01:30:57 +0500 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D0=BB=D0=B8=D0=B7=201.5.3:=20?= =?UTF-8?q?=D1=85=D0=BE=D1=82=D1=84=D0=B8=D0=BA=D1=81=D1=8B=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D1=82=D0=BE=D0=BA=D0=BE=D0=BB=D0=B0,=20=D1=81=D0=B8?= =?UTF-8?q?=D0=BD=D0=BA=D0=B0=20=D0=B8=20=D0=B1=D0=BE=D0=BB=D1=8C=D1=88?= =?UTF-8?q?=D0=B8=D1=85=20=D0=BB=D0=BE=D0=B3=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 4 +- .../rosetta/messenger/data/ReleaseNotes.kt | 25 ++------ .../com/rosetta/messenger/network/Protocol.kt | 30 +++++++++- .../messenger/network/ProtocolManager.kt | 34 ++++++++++- .../ui/components/AppleEmojiEditText.kt | 31 ++++++++-- .../messenger/ui/crashlogs/CrashLogsScreen.kt | 59 +++++++++++++++++-- 6 files changed, 148 insertions(+), 35 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index eae802f..0707ab1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -23,8 +23,8 @@ val gitShortSha = safeGitOutput("rev-parse", "--short", "HEAD") ?: "unknown" // ═══════════════════════════════════════════════════════════ // Rosetta versioning — bump here on each release // ═══════════════════════════════════════════════════════════ -val rosettaVersionName = "1.5.2" -val rosettaVersionCode = 54 // Increment on each release +val rosettaVersionName = "1.5.3" +val rosettaVersionCode = 55 // Increment on each release val customWebRtcAar = file("libs/libwebrtc-custom.aar") android { diff --git a/app/src/main/java/com/rosetta/messenger/data/ReleaseNotes.kt b/app/src/main/java/com/rosetta/messenger/data/ReleaseNotes.kt index eeaba7d..b38be38 100644 --- a/app/src/main/java/com/rosetta/messenger/data/ReleaseNotes.kt +++ b/app/src/main/java/com/rosetta/messenger/data/ReleaseNotes.kt @@ -17,27 +17,10 @@ object ReleaseNotes { val RELEASE_NOTICE = """ Update v$VERSION_PLACEHOLDER - - Перемотка голосовых полностью переработана в Telegram-style: drag по waveform и точный seek по отпусканию - - Устранены конфликты жестов у ГС: tap/drag/scrub больше не конфликтуют со swipe-to-reply и swipe-back - - Голосовой плеер доработан: стабильный scrub в паузе, корректный keepPaused, более надежный прогресс - - Добавлена очередь ГС внутри диалога с автопереходом к следующему голосовому по хронологии - - Улучшена совместимость payload голосовых (hex/base64 decode fallback) и восстановление аудиофайла из кэша - - Исправлено позиционирование и clipping кнопки записи в input-панели - - Добавлен haptic при старте записи и обновлены иконки записи voice/video - - В сайдбаре ограничен список аккаунтов (до 3) для более чистого Telegram-like layout - - Исправлен transition emoji -> keyboard: убран «пустой» зазор при закрытии emoji-панели - - В selection header чата добавлена кнопка Pin/Unpin для выбранного сообщения - - В Forward-пикере всегда показывается Saved Messages (даже если self-диалог ещё не создан) - - Переработаны media-permissions в attach/media picker: корректный permanently denied flow с переходом в Settings - - Улучшена инициализация аккаунта после login/unlock/create-account, устранён race «Sync postponed until account is initialized» - - Доработана синхронизация профиля аккаунта (name/username/verified), включая замену placeholder-имён - - Исправлен критичный баг отправки после верификации нового устройства на втором девайсе - - Исправлен reconnect overflow: устранена отрицательная задержка (-2147483648000ms) и дубли disconnect/reconnect - - Улучшена обработка device verification (ACCEPT/DECLINE) и reconnect-логика протокола - - Звонки: добавлен proximity manager (экран гаснет возле уха), добавлен WAKE_LOCK, учтён speaker on/off - - Звонки: рингтон теперь учитывает системный ringer mode (silent/vibrate), снижены ложные звуковые срабатывания - - Убраны дубли CALL-attachments у callee: источник call-события теперь единый (каноничный от caller) - - Групповые сообщения: fallback для plaintext-пакетов без group key и расширенная диагностика decrypt-ошибок + - Исправлен сценарий входа с нового устройства после отклонения верификации: повторный запрос теперь работает корректно без перезапуска приложения + - Стабилизирована синхронизация после ошибок обработки батча: добавлен безопасный retry без потери sync-курсора + - Исправлен вылет при открытии диалога с очень длинными сообщениями/логами + - Исправлен вылет в Crash Details при копировании больших логов; для полного лога добавлен экспорт через Share """.trimIndent() fun getNotice(version: String): String = diff --git a/app/src/main/java/com/rosetta/messenger/network/Protocol.kt b/app/src/main/java/com/rosetta/messenger/network/Protocol.kt index 14747b8..c482449 100644 --- a/app/src/main/java/com/rosetta/messenger/network/Protocol.kt +++ b/app/src/main/java/com/rosetta/messenger/network/Protocol.kt @@ -394,7 +394,35 @@ class Protocol( } } DeviceResolveSolution.DECLINE -> { - log("⛔ DEVICE VERIFICATION DECLINED (deviceId=${shortKey(resolve.deviceId, 12)})") + val stateAtDecline = _state.value + log( + "⛔ DEVICE VERIFICATION DECLINED (deviceId=${shortKey(resolve.deviceId, 12)}, state=$stateAtDecline)" + ) + + // Critical recovery: after DECLINE user may retry login without app restart. + // Keep socket session alive when possible, but leave DEVICE_VERIFICATION_REQUIRED + // state so next authenticate() is not ignored by startHandshake guards. + if ( + stateAtDecline == ProtocolState.DEVICE_VERIFICATION_REQUIRED || + stateAtDecline == ProtocolState.HANDSHAKING + ) { + launchLifecycleOperation("device_verification_declined") { + handshakeComplete = false + handshakeJob?.cancel() + packetQueue.clear() + if (webSocket != null) { + setState( + ProtocolState.CONNECTED, + "Device verification declined, waiting for retry" + ) + } else { + setState( + ProtocolState.DISCONNECTED, + "Device verification declined without active socket" + ) + } + } + } } } } diff --git a/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt b/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt index 3da4aaf..e1b6ed6 100644 --- a/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt +++ b/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt @@ -133,6 +133,8 @@ object ProtocolManager { // Tracks the tail of the sequential processing chain (like desktop's `tail` promise) @Volatile private var inboundQueueDrainJob: Job? = null private val inboundProcessingFailures = AtomicInteger(0) + private val inboundTasksInCurrentBatch = AtomicInteger(0) + private val fullFailureBatchStreak = AtomicInteger(0) private val syncBatchEndMutex = Mutex() // iOS parity: outgoing message retry mechanism. @@ -646,8 +648,15 @@ object ProtocolManager { private fun launchInboundPacketTask(block: suspend () -> Unit): Boolean { ensureInboundQueueDrainRunning() + val countAsBatchTask = syncBatchInProgress + if (countAsBatchTask) { + inboundTasksInCurrentBatch.incrementAndGet() + } val result = inboundTaskChannel.trySend(block) if (result.isFailure) { + if (countAsBatchTask) { + inboundTasksInCurrentBatch.decrementAndGet() + } markInboundProcessingFailure( "Failed to enqueue inbound task", result.exceptionOrNull() @@ -781,6 +790,8 @@ object ProtocolManager { syncRequestInFlight = false clearSyncRequestTimeout() inboundProcessingFailures.set(0) + inboundTasksInCurrentBatch.set(0) + fullFailureBatchStreak.set(0) addLog(reason) setSyncInProgress(false) retryWaitingMessages() @@ -902,6 +913,7 @@ object ProtocolManager { // subsequent 0x06 packets are dispatched by OkHttp's sequential callback. setSyncInProgress(true) inboundProcessingFailures.set(0) + inboundTasksInCurrentBatch.set(0) } SyncStatus.BATCH_END -> { addLog("🔄 SYNC BATCH_END — waiting for tasks to finish (ts=${packet.timestamp})") @@ -920,10 +932,30 @@ object ProtocolManager { return@launch } val failuresInBatch = inboundProcessingFailures.getAndSet(0) + val tasksInBatch = inboundTasksInCurrentBatch.getAndSet(0) + val fullBatchFailure = tasksInBatch > 0 && failuresInBatch >= tasksInBatch if (failuresInBatch > 0) { addLog( - "⚠️ SYNC batch had $failuresInBatch processing error(s), continuing with desktop sync cursor behavior" + "⚠️ SYNC batch had $failuresInBatch processing error(s) out of $tasksInBatch task(s)" ) + if (fullBatchFailure) { + val streak = fullFailureBatchStreak.incrementAndGet() + val fallbackCursor = messageRepository?.getLastSyncTimestamp() ?: 0L + if (streak <= 2) { + addLog( + "🛟 SYNC full-batch failure ($failuresInBatch/$tasksInBatch), keeping cursor=$fallbackCursor and retrying batch (streak=$streak)" + ) + sendSynchronize(fallbackCursor) + return@launch + } + addLog( + "⚠️ SYNC full-batch failure streak=$streak, advancing cursor to avoid deadlock" + ) + } else { + fullFailureBatchStreak.set(0) + } + } else { + fullFailureBatchStreak.set(0) } val repository = messageRepository // Desktop parity: save the cursor provided by BATCH_END and request next diff --git a/app/src/main/java/com/rosetta/messenger/ui/components/AppleEmojiEditText.kt b/app/src/main/java/com/rosetta/messenger/ui/components/AppleEmojiEditText.kt index 1306a59..1fce929 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/components/AppleEmojiEditText.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/components/AppleEmojiEditText.kt @@ -675,6 +675,9 @@ class AppleEmojiTextView @JvmOverloads constructor( companion object { private const val LARGE_TEXT_RENDER_THRESHOLD = 4000 + private const val HARD_TEXT_RENDER_LIMIT = 24_000 + private const val HARD_TEXT_RENDER_HEAD = 16_000 + private const val HARD_TEXT_RENDER_TAIL = 7_000 private val EMOJI_PATTERN = AppleEmojiEditTextView.EMOJI_PATTERN // 🔥 Паттерн для :emoji_XXXX: формата (из React Native) private val EMOJI_CODE_PATTERN = Pattern.compile(":emoji_([a-fA-F0-9_-]+):") @@ -693,6 +696,21 @@ class AppleEmojiTextView @JvmOverloads constructor( private val MENTION_PATTERN = Pattern.compile("(? LARGE_TEXT_RENDER_THRESHOLD + val renderText = safeRenderText(text) + val isLargeText = renderText.length > LARGE_TEXT_RENDER_THRESHOLD val processMentions = mentionsEnabled && !isLargeText val processLinks = linksEnabled && !isLargeText - val processEmoji = !isLargeText || containsEmojiHints(text) + val processEmoji = !isLargeText || containsEmojiHints(renderText) // Для длинных логов (без emoji/links/mentions) не запускаем дорогой regex/span пайплайн. if (!processEmoji && !processMentions && !processLinks) { - setText(text) + setText(renderText) return } // 🔥 Сначала заменяем :emoji_XXXX: на PNG изображения - val spannable = SpannableStringBuilder(text) + val spannable = SpannableStringBuilder(renderText) // Собираем все замены (чтобы не сбить индексы) data class EmojiMatch( @@ -892,7 +911,7 @@ class AppleEmojiTextView @JvmOverloads constructor( val emojiMatches = mutableListOf() // 1. Ищем :emoji_XXXX: формат - val codeMatcher = EMOJI_CODE_PATTERN.matcher(text) + val codeMatcher = EMOJI_CODE_PATTERN.matcher(renderText) while (codeMatcher.find()) { val rawCode = codeMatcher.group(1) ?: continue val unified = @@ -913,7 +932,7 @@ class AppleEmojiTextView @JvmOverloads constructor( val unicodeMatches = AppleEmojiAssetResolver.collectUnicodeMatches( context = context, - text = text, + text = renderText, occupiedRanges = occupiedRanges ) unicodeMatches.forEach { match -> diff --git a/app/src/main/java/com/rosetta/messenger/ui/crashlogs/CrashLogsScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/crashlogs/CrashLogsScreen.kt index f009f30..c96dc20 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/crashlogs/CrashLogsScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/crashlogs/CrashLogsScreen.kt @@ -1,5 +1,6 @@ package com.rosetta.messenger.ui.crashlogs +import android.content.Intent import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -25,12 +26,15 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.core.content.FileProvider import com.rosetta.messenger.utils.CrashReportManager +import java.io.File import java.text.SimpleDateFormat import java.util.* private const val MAX_CRASH_PREVIEW_CHARS = 180_000 private const val MAX_CRASH_PREVIEW_LINES = 2_500 +private const val MAX_CLIPBOARD_COPY_CHARS = 120_000 private fun buildCrashPreview(content: String): String { if (content.length <= MAX_CRASH_PREVIEW_CHARS) return content @@ -38,10 +42,41 @@ private fun buildCrashPreview(content: String): String { return buildString(clipped.length + 128) { append(clipped) append("\n\n--- LOG TRUNCATED IN VIEW ---\n") - append("Use the copy button to copy the full crash log.") + append("Use the share button to export the full crash log.") } } +private fun buildSafeClipboardLog(content: String): Pair { + if (content.length <= MAX_CLIPBOARD_COPY_CHARS) return content to false + val clipped = content.take(MAX_CLIPBOARD_COPY_CHARS).trimEnd() + val safeText = buildString(clipped.length + 220) { + append(clipped) + append("\n\n--- LOG TRUNCATED FOR CLIPBOARD ---\n") + append("Crash log is too large for clipboard transfer. ") + append("Use Share to export the full log file.") + } + return safeText to true +} + +private fun shareCrashLog(context: android.content.Context, crashReport: CrashReportManager.CrashReport) { + val exportDir = File(context.cacheDir, "shared_crash_reports").apply { mkdirs() } + val safeBaseName = crashReport.fileName + .substringBeforeLast(".") + .replace(Regex("[^a-zA-Z0-9._-]"), "_") + .ifBlank { "crash_log" } + val exportFile = File(exportDir, "${safeBaseName}_${System.currentTimeMillis()}.txt") + exportFile.writeText(crashReport.content) + val uri = FileProvider.getUriForFile(context, "${context.packageName}.provider", exportFile) + + val shareIntent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_SUBJECT, "Rosetta crash log: ${crashReport.fileName}") + putExtra(Intent.EXTRA_STREAM, uri) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + context.startActivity(Intent.createChooser(shareIntent, "Share crash log")) +} + /** * Экран для просмотра crash logs */ @@ -300,13 +335,29 @@ private fun CrashDetailScreen( actions = { IconButton( onClick = { - clipboardManager.setText(AnnotatedString(crashReport.content)) - Toast.makeText(context, "Full log copied", Toast.LENGTH_SHORT).show() + runCatching { + val (safeClipboardText, wasTruncated) = buildSafeClipboardLog(crashReport.content) + clipboardManager.setText(AnnotatedString(safeClipboardText)) + val message = if (wasTruncated) { + "Log is too large. Copied trimmed text. Use Share for full log." + } else { + "Full log copied" + } + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + }.onFailure { + Toast.makeText(context, "Failed to copy log", Toast.LENGTH_SHORT).show() + } } ) { Icon(Icons.Default.ContentCopy, contentDescription = "Copy Full Log") } - IconButton(onClick = { /* TODO: Share */ }) { + IconButton(onClick = { + runCatching { + shareCrashLog(context, crashReport) + }.onFailure { + Toast.makeText(context, "Failed to share crash log", Toast.LENGTH_SHORT).show() + } + }) { Icon(Icons.Default.Share, contentDescription = "Share") } IconButton(onClick = { showDeleteDialog = true }) {