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 8e92cbe..05beb49 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 @@ -3037,6 +3037,7 @@ fun ChatDetailScreen( else -> { LazyColumn( state = listState, + userScrollEnabled = !textSelectionHelper.movingHandle, modifier = Modifier.fillMaxSize() .nestedScroll( 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 4957225..5331dc8 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 @@ -402,6 +402,9 @@ fun MessageBubble( else Color(0xFF2196F3) // Стандартный Material Blue для входящих } var textViewRef by remember { mutableStateOf(null) } + val selectionDragEndHandler: (() -> Unit)? = if (textSelectionHelper != null) { + { textSelectionHelper.hideMagnifier(); textSelectionHelper.endHandleDrag() } + } else null val linksEnabled = !isSelectionMode val textClickHandler: (() -> Unit)? = onClick val mentionClickHandler: ((String) -> Unit)? = @@ -1070,7 +1073,7 @@ fun MessageBubble( onLongClick = onLongClick, // 🔥 Long press для selection onViewCreated = { textViewRef = it }, - onTextLongPress = if (textSelectionHelper != null && !isSelectionMode) { touchX, touchY -> + onTextLongPress = if (textSelectionHelper != null && isSelected) { touchX, touchY -> val info = textViewRef?.getLayoutInfo() if (info != null) { textSelectionHelper.startSelection( @@ -1081,7 +1084,18 @@ fun MessageBubble( view = textViewRef ) } - } else null + } else null, + onSelectionDrag = if (textSelectionHelper != null) { tx, ty -> + textSelectionHelper.moveHandle( + (tx - textSelectionHelper.overlayWindowX), + (ty - textSelectionHelper.overlayWindowY) + ) + textSelectionHelper.showMagnifier( + (tx - textSelectionHelper.overlayWindowX), + (ty - textSelectionHelper.overlayWindowY) + ) + } else null, + onSelectionDragEnd = selectionDragEndHandler ) }, timeContent = { @@ -1287,7 +1301,7 @@ fun MessageBubble( onLongClick = onLongClick, // 🔥 Long press для selection onViewCreated = { textViewRef = it }, - onTextLongPress = if (textSelectionHelper != null && !isSelectionMode) { touchX, touchY -> + onTextLongPress = if (textSelectionHelper != null && isSelected) { touchX, touchY -> val info = textViewRef?.getLayoutInfo() if (info != null) { textSelectionHelper.startSelection( @@ -1298,7 +1312,18 @@ fun MessageBubble( view = textViewRef ) } - } else null + } else null, + onSelectionDrag = if (textSelectionHelper != null) { tx, ty -> + textSelectionHelper.moveHandle( + (tx - textSelectionHelper.overlayWindowX), + (ty - textSelectionHelper.overlayWindowY) + ) + textSelectionHelper.showMagnifier( + (tx - textSelectionHelper.overlayWindowX), + (ty - textSelectionHelper.overlayWindowY) + ) + } else null, + onSelectionDragEnd = selectionDragEndHandler ) }, timeContent = { 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 8e7f8ba..c556b6f 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 @@ -72,6 +72,11 @@ class TextSelectionHelper { private set var movingHandleStart by mutableStateOf(false) private set + // Telegram: isOneTouch = true during initial long-press drag (before finger lifts) + var isOneTouch by mutableStateOf(false) + private set + // Telegram: direction not determined yet — first drag decides start or end handle + private var movingDirectionSettling = false private var movingOffsetX = 0f private var movingOffsetY = 0f @@ -117,6 +122,13 @@ class TextSelectionHelper { isActive = true handleViewProgress = 1f + // Telegram: immediately enter drag mode — user can drag without lifting finger + movingHandle = true + movingDirectionSettling = true + isOneTouch = true + movingOffsetX = 0f + movingOffsetY = 0f + view?.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) showToolbar = false } @@ -161,11 +173,27 @@ class TextSelectionHelper { val y = (touchY + movingOffsetY).toInt() val offset = getCharOffsetFromCoords(x, y) if (offset < 0) return + + // Telegram: first drag determines which handle to move + if (movingDirectionSettling) { + if (offset < selectionStart) { + movingDirectionSettling = false + movingHandleStart = true + } else if (offset > selectionEnd) { + movingDirectionSettling = false + movingHandleStart = false + } else { + return // still within selected word, wait for more movement + } + } + if (movingHandleStart) updateSelectionStart(offset) else updateSelectionEnd(offset) } fun endHandleDrag() { movingHandle = false + movingDirectionSettling = false + isOneTouch = false showFloatingToolbar() } @@ -272,6 +300,8 @@ class TextSelectionHelper { isActive = false handleViewProgress = 0f movingHandle = false + movingDirectionSettling = false + isOneTouch = false showToolbar = false hideMagnifier() } @@ -301,12 +331,20 @@ private fun FloatingToolbarPopup( val info = helper.layoutInfo ?: return val layout = info.layout val startLine = layout.getLineForOffset(helper.selectionStart.coerceIn(0, info.text.length)) - val toolbarY = (layout.getLineTop(startLine) + info.windowY - 52).coerceAtLeast(0) - val toolbarX = ((helper.startHandleX + helper.endHandleX) / 2f - 80f).coerceAtLeast(0f) + + // Toolbar position: centered above selection, in overlay-local coords + // startHandleX/endHandleX are already overlay-local + 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) Popup( alignment = Alignment.TopStart, - offset = IntOffset(toolbarX.toInt(), toolbarY) + offset = IntOffset(toolbarX.toInt(), toolbarY.toInt()) ) { Row( modifier = Modifier 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 5643d3f..1306a59 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 @@ -559,6 +559,8 @@ fun AppleEmojiText( onClick: (() -> Unit)? = null, // 🔥 Обычный tap (selection mode в MessageBubble) onLongClick: (() -> Unit)? = null, // 🔥 Callback для long press (selection в MessageBubble) onTextLongPress: ((touchX: Int, touchY: Int) -> Unit)? = null, + onSelectionDrag: ((touchX: Int, touchY: Int) -> Unit)? = null, + onSelectionDragEnd: (() -> Unit)? = null, onViewCreated: ((com.rosetta.messenger.ui.components.AppleEmojiTextView) -> Unit)? = null, minHeightMultiplier: Float = 1.5f ) { @@ -604,6 +606,8 @@ fun AppleEmojiText( setOnMentionClickListener(onMentionClick) setOnClickableSpanPressStartListener(onClickableSpanPressStart) onTextLongPressCallback = onTextLongPress + this.onSelectionDrag = onSelectionDrag + this.onSelectionDragEnd = onSelectionDragEnd onViewCreated?.invoke(this) // In link/mention mode, keep span clicks exclusive to avoid parent bubble menu tap. val canUseTextViewClick = !enableLinks @@ -639,6 +643,8 @@ fun AppleEmojiText( view.setOnMentionClickListener(onMentionClick) view.setOnClickableSpanPressStartListener(onClickableSpanPressStart) view.onTextLongPressCallback = onTextLongPress + view.onSelectionDrag = onSelectionDrag + view.onSelectionDragEnd = onSelectionDragEnd // In link/mention mode, keep span clicks exclusive to avoid parent bubble menu tap. val canUseTextViewClick = !enableLinks view.setOnClickListener( @@ -701,8 +707,12 @@ class AppleEmojiTextView @JvmOverloads constructor( // 🔥 Long press callback для selection в MessageBubble var onLongClickCallback: (() -> Unit)? = null var onTextLongPressCallback: ((touchX: Int, touchY: Int) -> Unit)? = null + // Telegram flow: forward drag/up events after long press fires + var onSelectionDrag: ((touchX: Int, touchY: Int) -> Unit)? = null + var onSelectionDragEnd: (() -> Unit)? = null private var downOnClickableSpan: Boolean = false private var suppressPerformClickOnce: Boolean = false + private var selectionDragActive: Boolean = false // 🔥 GestureDetector для обработки long press поверх LinkMovementMethod private val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() { @@ -710,6 +720,8 @@ class AppleEmojiTextView @JvmOverloads constructor( if (downOnClickableSpan) return if (onTextLongPressCallback != null) { onTextLongPressCallback?.invoke(e.rawX.toInt(), e.rawY.toInt()) + selectionDragActive = true + parent?.requestDisallowInterceptTouchEvent(true) // block scroll during drag } else { onLongClickCallback?.invoke() } @@ -730,21 +742,33 @@ class AppleEmojiTextView @JvmOverloads constructor( MotionEvent.ACTION_DOWN -> { downOnClickableSpan = isTouchOnClickableSpan(event) suppressPerformClickOnce = downOnClickableSpan + selectionDragActive = false if (downOnClickableSpan) { clickableSpanPressStartCallback?.invoke() parent?.requestDisallowInterceptTouchEvent(true) } } + MotionEvent.ACTION_MOVE -> { + if (selectionDragActive) { + onSelectionDrag?.invoke(event.rawX.toInt(), event.rawY.toInt()) + return true + } + } MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> { + if (selectionDragActive) { + selectionDragActive = false + onSelectionDragEnd?.invoke() + downOnClickableSpan = false + parent?.requestDisallowInterceptTouchEvent(false) + return true + } downOnClickableSpan = false parent?.requestDisallowInterceptTouchEvent(false) } } - // Позволяем GestureDetector обработать событие (для long press) gestureDetector.onTouchEvent(event) - // Передаем событие дальше для обработки ссылок return super.dispatchTouchEvent(event) }