diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/AnimatedEmoji.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/AnimatedEmoji.kt index 5e96d1da..7a934475 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/AnimatedEmoji.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/AnimatedEmoji.kt @@ -6,6 +6,7 @@ import android.icu.text.BreakIterator import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.appendInlineContent import androidx.compose.material3.Text @@ -19,6 +20,10 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.Placeholder import androidx.compose.ui.text.PlaceholderVerticalAlign import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.TextUnit import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver @@ -103,9 +108,10 @@ fun AnimatedEmoji(emoji: String, fontSize: TextUnit, modifier: Modifier = Modifi /** Полные графемы сохраняют модификаторы кожи и ZWJ-последовательности обычными Unicode-смайликами. */ @Composable -fun EmojiText(text: String, fontSize: TextUnit, color: Color, modifier: Modifier = Modifier) { +fun EmojiText(text: String, fontSize: TextUnit, color: Color, modifier: Modifier = Modifier, + onInviteLink: ((String) -> Unit)? = null) { val annotated = remember(text) { - buildAnnotatedString { + val emojiText = buildAnnotatedString { val iterator = BreakIterator.getCharacterInstance(Locale.US) iterator.setText(text) var start = iterator.first() @@ -118,6 +124,14 @@ fun EmojiText(text: String, fontSize: TextUnit, color: Color, modifier: Modifier end = iterator.next() } } + buildAnnotatedString { + append(emojiText) + Regex("utun://[A-Za-z0-9+/]+={0,2}").findAll(emojiText.text).forEach { match -> + addStringAnnotation("invite", match.value, match.range.first, match.range.last + 1) + addStyle(SpanStyle(color = Color(0xFF3390EC), textDecoration = TextDecoration.Underline), + match.range.first, match.range.last + 1) + } + } } val inline = remember(fontSize, color) { animatedEmojiAssets.values.associateWith { @@ -126,5 +140,19 @@ fun EmojiText(text: String, fontSize: TextUnit, color: Color, modifier: Modifier } } } - Text(annotated, inlineContent = inline, fontSize = fontSize, color = color, modifier = modifier) + var layout by remember { mutableStateOf(null) } + val callback by rememberUpdatedState(onInviteLink) + val links = annotated.getStringAnnotations("invite", 0, annotated.length) + val linkModifier = if (onInviteLink == null || links.isEmpty()) modifier else modifier.pointerInput(annotated) { + detectTapGestures { point -> + val result = layout ?: return@detectTapGestures + val line = result.getLineForVerticalPosition(point.y) + if (point.y < 0 || point.y >= result.size.height || point.x < result.getLineLeft(line) || + point.x >= result.getLineRight(line)) return@detectTapGestures + val offset = result.getOffsetForPosition(point) + annotated.getStringAnnotations("invite", offset, offset).firstOrNull()?.let { callback?.invoke(it.item) } + } + } + Text(annotated, inlineContent = inline, fontSize = fontSize, color = color, modifier = linkModifier, + onTextLayout = { layout = it }) } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/ConversationMessages.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/ConversationMessages.kt index 45eb8a40..c35a9c42 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/ConversationMessages.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/ConversationMessages.kt @@ -27,7 +27,8 @@ fun ConversationMessages(target: ConversationTarget, messages: List, st playbackState: ChatViewModel.PlaybackState, modifier: Modifier = Modifier, onPlayVoice: (String, Int) -> Unit, onSeekVoice: (String, Int, Int) -> Unit, onOpenFile: (Message) -> Unit, onPlayVideo: (Message) -> Unit, - onDownloadFile: (Message) -> Unit = {}, onOpenImage: (Message) -> Unit = {}) { + onDownloadFile: (Message) -> Unit = {}, onOpenImage: (Message) -> Unit = {}, + onInviteLink: (String) -> Unit = {}) { val background = ImageBitmap.imageResource(R.drawable.chat_background) LaunchedEffect(background) { LogManager.addLog("DEBUG", "UI", "Chat background ready: desktop bg.png tile=${background.width}x${background.height}") @@ -44,7 +45,7 @@ fun ConversationMessages(target: ConversationTarget, messages: List, st LazyColumn(modifier = modifier.then(backgroundModifier), state = state, reverseLayout = true) { item { Spacer(Modifier.height(8.dp)) } items(messages.reversed(), key = { "${target.nativeId}:${it.isOutgoing}:${it.id}" }) { message -> - MessageBubble(message, playbackState, onPlayVoice, onSeekVoice, onDownloadFile, onOpenFile, onOpenImage, onPlayVideo) + MessageBubble(message, playbackState, onPlayVoice, onSeekVoice, onDownloadFile, onOpenFile, onOpenImage, onPlayVideo, onInviteLink) } } } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt index 4b8c086f..02075213 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt @@ -102,7 +102,8 @@ fun MessageBubble(message: Message, playbackState: ChatViewModel.PlaybackState = onDownloadFile: (Message) -> Unit = {}, onOpenFile: (Message) -> Unit = {}, onOpenImage: (Message) -> Unit = {}, - onPlayVideo: (Message) -> Unit = {}) { + onPlayVideo: (Message) -> Unit = {}, + onInviteLink: (String) -> Unit = {}) { val bgColor = if (message.isOutgoing) ChatColors.OutgoingBubble else ChatColors.IncomingBubble val textColor = ChatColors.Text val alignment = if (message.isOutgoing) Arrangement.End else Arrangement.Start @@ -158,7 +159,7 @@ fun MessageBubble(message: Message, playbackState: ChatViewModel.PlaybackState = isImage -> ImageBubble(message, textColor, onDownloadFile, onOpenImage) isMedia -> FileBubble(message, textColor, onDownloadFile, onOpenFile) else -> { - EmojiText(message.text, fontSize = 17.sp, color = textColor) + EmojiText(message.text, fontSize = 17.sp, color = textColor, onInviteLink = onInviteLink) } }