From b1fc623f5e7c136c1638ce8d864a44acaac3ee84 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Sun, 12 Apr 2026 23:05:55 +0500 Subject: [PATCH] =?UTF-8?q?=D0=92=D1=8B=D0=B4=D0=B5=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D1=82=D0=B5=D0=BA=D1=81=D1=82=D0=B0=20+=20?= =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=20ANR=20=D0=BF=D1=80=D0=B8=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BF=D0=B8=D1=81=D0=B8=20=D0=93=D0=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../messenger/data/MessageRepository.kt | 1 - .../messenger/ui/chats/ChatDetailScreen.kt | 4 ++ .../chats/components/ChatDetailComponents.kt | 3 +- .../chats/components/TextSelectionHelper.kt | 62 +++++++++++++++---- .../ui/chats/input/ChatDetailInput.kt | 57 ++++++++++------- 5 files changed, 90 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt b/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt index 8c5fbad..18366d9 100644 --- a/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt +++ b/app/src/main/java/com/rosetta/messenger/data/MessageRepository.kt @@ -30,7 +30,6 @@ data class Message( val replyToMessageId: String? = null ) -/** UI модель диалога */ data class Dialog( val opponentKey: String, val opponentTitle: String, diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt index 05beb49..0633e54 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt @@ -615,6 +615,8 @@ fun ChatDetailScreen( showImageViewer, showMediaPicker, showEmojiPicker, + textSelectionHelper.isActive, + textSelectionHelper.movingHandle, pendingCameraPhotoUri, pendingGalleryImages, showInAppCamera, @@ -624,6 +626,8 @@ fun ChatDetailScreen( showImageViewer || showMediaPicker || showEmojiPicker || + textSelectionHelper.isActive || + textSelectionHelper.movingHandle || pendingCameraPhotoUri != null || pendingGalleryImages.isNotEmpty() || showInAppCamera || diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt index 5331dc8..852c411 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt @@ -480,8 +480,9 @@ fun MessageBubble( Box( modifier = - Modifier.fillMaxWidth().pointerInput(isSystemSafeChat) { + Modifier.fillMaxWidth().pointerInput(isSystemSafeChat, textSelectionHelper?.isActive) { if (isSystemSafeChat) return@pointerInput + if (textSelectionHelper?.isActive == true) return@pointerInput // 🔥 Простой горизонтальный свайп для reply // Используем detectHorizontalDragGestures который лучше работает со // скроллом diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/TextSelectionHelper.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/TextSelectionHelper.kt index c556b6f..cbb3430 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/TextSelectionHelper.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/TextSelectionHelper.kt @@ -85,6 +85,9 @@ class TextSelectionHelper { var endHandleX by mutableFloatStateOf(0f) var endHandleY by mutableFloatStateOf(0f) + // Back gesture callback — registered/unregistered by overlay + var backCallback: Any? = null + // Overlay position in window — set by TextSelectionOverlay var overlayWindowX = 0f var overlayWindowY = 0f @@ -330,17 +333,18 @@ private fun FloatingToolbarPopup( val info = helper.layoutInfo ?: return val layout = info.layout + val density = LocalDensity.current val startLine = layout.getLineForOffset(helper.selectionStart.coerceIn(0, info.text.length)) - // Toolbar position: centered above selection, in overlay-local coords - // startHandleX/endHandleX are already overlay-local + // Toolbar positioned ABOVE selection top, in overlay-local coordinates val selectionCenterX = (helper.startHandleX + helper.endHandleX) / 2f val selectionTopY = layout.getLineTop(startLine).toFloat() + (info.windowY - helper.overlayWindowY) - val toolbarHeight = 40f // approximate toolbar height in px - val toolbarWidth = 160f // approximate toolbar width in px - val toolbarX = (selectionCenterX - toolbarWidth / 2f).coerceAtLeast(8f) - val toolbarY = (selectionTopY - toolbarHeight - 12f).coerceAtLeast(0f) + // Toolbar is ~48dp tall + 8dp gap above selection + val toolbarOffsetPx = with(density) { 56.dp.toPx() } + val toolbarWidthPx = with(density) { 200.dp.toPx() } + val toolbarX = (selectionCenterX - toolbarWidthPx / 2f).coerceAtLeast(with(density) { 8.dp.toPx() }) + val toolbarY = (selectionTopY - toolbarOffsetPx).coerceAtLeast(0f) Popup( alignment = Alignment.TopStart, @@ -395,6 +399,36 @@ fun TextSelectionOverlay( val handleInsetPx = with(density) { HandleInset.toPx() } val highlightCornerPx = with(density) { HighlightCorner.toPx() } + // Block predictive back gesture completely during text selection. + // BackHandler alone doesn't prevent the swipe animation on Android 13+ + // with enableOnBackInvokedCallback=true. We must register an + // OnBackInvokedCallback at PRIORITY_OVERLAY to fully suppress it. + val activity = LocalContext.current as? android.app.Activity + LaunchedEffect(helper.isActive) { + if (android.os.Build.VERSION.SDK_INT >= 33 && activity != null) { + if (helper.isActive) { + val cb = android.window.OnBackInvokedCallback { /* consumed, do nothing */ } + activity.onBackInvokedDispatcher.registerOnBackInvokedCallback( + android.window.OnBackInvokedDispatcher.PRIORITY_OVERLAY, cb + ) + helper.backCallback = cb + } else { + helper.backCallback?.let { cb -> + runCatching { + activity.onBackInvokedDispatcher.unregisterOnBackInvokedCallback( + cb as android.window.OnBackInvokedCallback + ) + } + } + helper.backCallback = null + } + } + } + // Fallback for Android < 13 + androidx.activity.compose.BackHandler(enabled = helper.isActive) { + // consumed — no navigation back while selecting + } + Box( modifier = modifier .fillMaxSize() @@ -477,18 +511,22 @@ fun TextSelectionOverlay( val startLine = layout.getLineForOffset(startOffset) val endLine = layout.getLineForOffset(endOffset) + // Padding around highlight for breathing room + val padH = 3.dp.toPx() + val padV = 2.dp.toPx() + for (line in startLine..endLine) { - val lineTop = layout.getLineTop(line).toFloat() + offsetY - val lineBottom = layout.getLineBottom(line).toFloat() + offsetY + val lineTop = layout.getLineTop(line).toFloat() + offsetY - padV + val lineBottom = layout.getLineBottom(line).toFloat() + offsetY + padV val left = if (line == startLine) { - layout.getPrimaryHorizontal(startOffset) + offsetX + layout.getPrimaryHorizontal(startOffset) + offsetX - padH } else { - layout.getLineLeft(line) + offsetX + layout.getLineLeft(line) + offsetX - padH } val right = if (line == endLine) { - layout.getPrimaryHorizontal(endOffset) + offsetX + layout.getPrimaryHorizontal(endOffset) + offsetX + padH } else { - layout.getLineRight(line) + offsetX + layout.getLineRight(line) + offsetX + padH } drawRoundRect( color = HighlightColor, diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/input/ChatDetailInput.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/input/ChatDetailInput.kt index 1714a77..859abab 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/input/ChatDetailInput.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/input/ChatDetailInput.kt @@ -1147,31 +1147,37 @@ fun MessageInputBar( voiceRecordStartedAtMs = 0L voiceElapsedMs = 0L voiceWaves = emptyList() - - var recordedOk = false - if (recorder != null) { - recordedOk = runCatching { - recorder.stop() - true - }.getOrDefault(false) - runCatching { recorder.reset() } - runCatching { recorder.release() } - } - - if (send && recordedOk && outputFile != null && outputFile.exists() && outputFile.length() > 0L) { - val voiceHex = - runCatching { bytesToHexLower(outputFile.readBytes()) }.getOrDefault("") - if (voiceHex.isNotBlank()) { - onSendVoiceMessage( - voiceHex, - durationSnapshot, - compressVoiceWaves(wavesSnapshot, 35) - ) - } - } - runCatching { outputFile?.delete() } resetGestureState() setRecordUiState(RecordUiState.IDLE, "stop(send=$send)") + + // Heavy I/O off main thread to prevent ANR + scope.launch(kotlinx.coroutines.Dispatchers.IO) { + var recordedOk = false + if (recorder != null) { + recordedOk = runCatching { + recorder.stop() + true + }.getOrDefault(false) + runCatching { recorder.reset() } + runCatching { recorder.release() } + } + + if (send && recordedOk && outputFile != null && outputFile.exists() && outputFile.length() > 0L) { + val voiceHex = + runCatching { bytesToHexLower(outputFile.readBytes()) }.getOrDefault("") + if (voiceHex.isNotBlank()) { + kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.Main) { + onSendVoiceMessage( + voiceHex, + durationSnapshot, + compressVoiceWaves(wavesSnapshot, 35) + ) + } + } + } + runCatching { outputFile?.delete() } + } + inputJumpLog( "stopVoiceRecording end send=$send mode=$recordMode state=$recordUiState voice=$isVoiceRecording kb=$isKeyboardVisible " + "emojiBox=${coordinator.isEmojiBoxVisible} panelH=$inputPanelHeightPx " + @@ -1203,8 +1209,13 @@ fun MessageInputBar( recorder.setAudioEncodingBitRate(32_000) recorder.setAudioSamplingRate(48_000) recorder.setOutputFile(output.absolutePath) + recorder.setMaxDuration(15 * 60 * 1000) // 15 min safety limit recorder.prepare() recorder.start() + recorder.setOnErrorListener { _, what, extra -> + inputJumpLog("MediaRecorder error what=$what extra=$extra") + stopVoiceRecording(send = false) + } voiceRecorder = recorder voiceOutputFile = output