Make invite links clickable in Android text with inline animated emoji
This commit is contained in:
1 parent
442a2a7b04
commit
68103d0aca
3 files changed
+37
-7
No files matched your search
+31
-3
@@ -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 })
|
||||
}
|
||||
+3
-2
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user