feat: интегрировать TextSelectionHelper в ChatDetailScreen и MessageBubble

- TextSelectionHelper инстанс в ChatDetailScreen
- TextSelectionOverlay поверх LazyColumn
- Clear selection при scroll и при message selection mode
- onTextLongPress + onViewCreated проброшены через MessageBubble к AppleEmojiText

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-12 15:12:16 +05:00
parent a10482b794
commit 7fcf1195e1
2 changed files with 63 additions and 12 deletions

View File

@@ -394,6 +394,9 @@ fun ChatDetailScreen(
var longPressSuppressedMessageId by remember { mutableStateOf<String?>(null) } var longPressSuppressedMessageId by remember { mutableStateOf<String?>(null) }
var longPressSuppressUntilMs by remember { mutableLongStateOf(0L) } var longPressSuppressUntilMs by remember { mutableLongStateOf(0L) }
// 🔤 TEXT SELECTION - Telegram-style character-level selection
val textSelectionHelper = remember { com.rosetta.messenger.ui.chats.components.TextSelectionHelper() }
// 💬 MESSAGE CONTEXT MENU STATE // 💬 MESSAGE CONTEXT MENU STATE
var contextMenuMessage by remember { mutableStateOf<ChatMessage?>(null) } var contextMenuMessage by remember { mutableStateOf<ChatMessage?>(null) }
var showContextMenu by remember { mutableStateOf(false) } var showContextMenu by remember { mutableStateOf(false) }
@@ -838,6 +841,7 @@ fun ChatDetailScreen(
// иначе при двойном колбэке (text + bubble) сообщение мгновенно "откатывается". // иначе при двойном колбэке (text + bubble) сообщение мгновенно "откатывается".
val selectMessageOnLongPress: (messageId: String, canSelect: Boolean) -> Unit = val selectMessageOnLongPress: (messageId: String, canSelect: Boolean) -> Unit =
{ messageId, canSelect -> { messageId, canSelect ->
textSelectionHelper.clear()
if (canSelect && !selectedMessages.contains(messageId)) { if (canSelect && !selectedMessages.contains(messageId)) {
selectedMessages = selectedMessages + messageId selectedMessages = selectedMessages + messageId
} }
@@ -886,6 +890,13 @@ fun ChatDetailScreen(
} }
} }
// 🔤 Сброс текстового выделения при скролле
LaunchedEffect(listState.isScrollInProgress) {
if (listState.isScrollInProgress && textSelectionHelper.isActive) {
textSelectionHelper.clear()
}
}
// 🔥 Display reply messages - получаем полную информацию о сообщениях для reply // 🔥 Display reply messages - получаем полную информацию о сообщениях для reply
val displayReplyMessages = val displayReplyMessages =
remember(replyMessages, messages) { remember(replyMessages, messages) {
@@ -3164,6 +3175,8 @@ fun ChatDetailScreen(
MessageBubble( MessageBubble(
message = message =
message, message,
textSelectionHelper =
textSelectionHelper,
isDarkTheme = isDarkTheme =
isDarkTheme, isDarkTheme,
hasWallpaper = hasWallpaper =
@@ -3644,6 +3657,11 @@ fun ChatDetailScreen(
} }
} }
} }
// 🔤 Text selection overlay
com.rosetta.messenger.ui.chats.components.TextSelectionOverlay(
helper = textSelectionHelper,
modifier = Modifier.fillMaxSize()
)
} }
} }
} }

View File

@@ -320,6 +320,7 @@ fun TypingIndicator(
@Composable @Composable
fun MessageBubble( fun MessageBubble(
message: ChatMessage, message: ChatMessage,
textSelectionHelper: com.rosetta.messenger.ui.chats.components.TextSelectionHelper? = null,
isDarkTheme: Boolean, isDarkTheme: Boolean,
hasWallpaper: Boolean = false, hasWallpaper: Boolean = false,
isSystemSafeChat: Boolean = false, isSystemSafeChat: Boolean = false,
@@ -400,6 +401,7 @@ fun MessageBubble(
if (message.isOutgoing) Color(0xFFB3E5FC) // Светло-голубой на синем фоне if (message.isOutgoing) Color(0xFFB3E5FC) // Светло-голубой на синем фоне
else Color(0xFF2196F3) // Стандартный Material Blue для входящих else Color(0xFF2196F3) // Стандартный Material Blue для входящих
} }
var textViewRef by remember { mutableStateOf<com.rosetta.messenger.ui.components.AppleEmojiTextView?>(null) }
val linksEnabled = !isSelectionMode val linksEnabled = !isSelectionMode
val textClickHandler: (() -> Unit)? = onClick val textClickHandler: (() -> Unit)? = onClick
val mentionClickHandler: ((String) -> Unit)? = val mentionClickHandler: ((String) -> Unit)? =
@@ -1066,7 +1068,20 @@ fun MessageBubble(
onClick = onClick =
textClickHandler, textClickHandler,
onLongClick = onLongClick =
onLongClick // 🔥 Long press для selection onLongClick, // 🔥 Long press для selection
onViewCreated = { textViewRef = it },
onTextLongPress = if (textSelectionHelper != null && !isSelectionMode) { touchX, touchY ->
val info = textViewRef?.getLayoutInfo()
if (info != null) {
textSelectionHelper.startSelection(
messageId = message.id,
info = info,
touchX = touchX,
touchY = touchY,
view = textViewRef
)
}
} else null
) )
}, },
timeContent = { timeContent = {
@@ -1157,12 +1172,21 @@ fun MessageBubble(
suppressBubbleTapFromSpan, suppressBubbleTapFromSpan,
onClick = textClickHandler, onClick = textClickHandler,
onLongClick = onLongClick =
onLongClick // 🔥 onLongClick, // 🔥 Long press для selection
// Long onViewCreated = { textViewRef = it },
// press onTextLongPress = if (textSelectionHelper != null && !isSelectionMode) { touchX, touchY ->
// для val info = textViewRef?.getLayoutInfo()
// selection if (info != null) {
) textSelectionHelper.startSelection(
messageId = message.id,
info = info,
touchX = touchX,
touchY = touchY,
view = textViewRef
)
}
} else null
)
}, },
timeContent = { timeContent = {
Row( Row(
@@ -1261,11 +1285,20 @@ fun MessageBubble(
suppressBubbleTapFromSpan, suppressBubbleTapFromSpan,
onClick = textClickHandler, onClick = textClickHandler,
onLongClick = onLongClick =
onLongClick // 🔥 onLongClick, // 🔥 Long press для selection
// Long onViewCreated = { textViewRef = it },
// press onTextLongPress = if (textSelectionHelper != null && !isSelectionMode) { touchX, touchY ->
// для val info = textViewRef?.getLayoutInfo()
// selection if (info != null) {
textSelectionHelper.startSelection(
messageId = message.id,
info = info,
touchX = touchX,
touchY = touchY,
view = textViewRef
)
}
} else null
) )
}, },
timeContent = { timeContent = {