Browse Source

Make invite links clickable in Android text with inline animated emoji

master
evgeny 5 days ago
parent
commit
68103d0aca
  1. 34
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/AnimatedEmoji.kt
  2. 5
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/ConversationMessages.kt
  3. 5
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt

34
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<TextLayoutResult?>(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 })
}

5
tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/ConversationMessages.kt

@ -27,7 +27,8 @@ fun ConversationMessages(target: ConversationTarget, messages: List<Message>, 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<Message>, 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)
}
}
}

5
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)
}
}

Loading…
Cancel
Save