Browse Source

Annotate Android web links without altering addresses containing emoji

master
evgeny 6 days ago
parent
commit
c191b777b7
  1. 55
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/AnimatedEmoji.kt
  2. 6
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt

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

@ -18,6 +18,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.PlaceholderVerticalAlign
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.TextLayoutResult
@ -33,6 +34,7 @@ import com.airbnb.lottie.compose.LottieConstants
import com.airbnb.lottie.compose.rememberLottieComposition
import com.airbnb.lottie.compose.rememberLottieAnimatable
import com.utun.chat.data.LogManager
import com.utun.chat.data.MessageLinks
import java.util.Locale
import kotlin.math.ceil
@ -106,31 +108,37 @@ fun AnimatedEmoji(emoji: String, fontSize: TextUnit, modifier: Modifier = Modifi
}
}
/** Преобразовать только обычный текст: адреса остаются точными, без emoji placeholders. */
private fun AnnotatedString.Builder.appendEmojiText(text: String) {
val iterator = BreakIterator.getCharacterInstance(Locale.US)
iterator.setText(text)
var start = iterator.first()
var end = iterator.next()
while (end != BreakIterator.DONE) {
val cluster = text.substring(start, end)
val asset = animationAsset(cluster)
if (asset == null) append(cluster) else appendInlineContent(asset, cluster)
start = end
end = iterator.next()
}
}
/** Полные графемы сохраняют модификаторы кожи и ZWJ-последовательности обычными Unicode-смайликами. */
@Composable
fun EmojiText(text: String, fontSize: TextUnit, color: Color, modifier: Modifier = Modifier,
onInviteLink: ((String) -> Unit)? = null) {
onLink: ((String) -> Unit)? = null) {
val annotated = remember(text) {
val emojiText = buildAnnotatedString {
val iterator = BreakIterator.getCharacterInstance(Locale.US)
iterator.setText(text)
var start = iterator.first()
var end = iterator.next()
while (end != BreakIterator.DONE) {
val cluster = text.substring(start, end)
val asset = animationAsset(cluster)
if (asset == null) append(cluster) else appendInlineContent(asset, cluster)
start = end
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)
var position = 0
for (link in MessageLinks.parse(text)) {
appendEmojiText(text.substring(position, link.start))
val start = length
append(link.url)
addStringAnnotation("link", link.url, start, length)
addStyle(SpanStyle(color = Color(0xFF3390EC), textDecoration = TextDecoration.Underline), start, length)
position = link.end
}
appendEmojiText(text.substring(position))
}
}
val inline = remember(fontSize, color) {
@ -141,16 +149,17 @@ fun EmojiText(text: String, fontSize: TextUnit, 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) {
val callback by rememberUpdatedState(onLink)
val links = annotated.getStringAnnotations("link", 0, annotated.length)
val linkModifier = if (onLink == 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) }
annotated.getStringAnnotations("link", offset, offset).firstOrNull { offset >= it.start && offset < it.end }
?.let { callback?.invoke(it.item) }
}
}
Text(annotated, inlineContent = inline, fontSize = fontSize, color = color, modifier = linkModifier,

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

@ -109,7 +109,7 @@ fun MessageBubble(message: Message, playbackState: ChatViewModel.PlaybackState =
onOpenFile: (Message) -> Unit = {},
onOpenImage: (Message) -> Unit = {},
onPlayVideo: (Message) -> Unit = {},
onInviteLink: (String) -> Unit = {}) {
onLink: (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
@ -170,7 +170,7 @@ fun MessageBubble(message: Message, playbackState: ChatViewModel.PlaybackState =
Text(message.text, fontSize = 17.sp, color = Color(0xFF3390EC), textDecoration = TextDecoration.Underline,
maxLines = 1, overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth().combinedClickable(
onClick = { onInviteLink(message.text) },
onClick = { onLink(message.text) },
onLongClickLabel = stringResource(R.string.copy_link),
onLongClick = {
clipboard.setText(AnnotatedString(message.text))
@ -182,7 +182,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, onInviteLink = onInviteLink)
EmojiText(message.text, fontSize = 17.sp, color = textColor, onLink = onLink)
}
}

Loading…
Cancel
Save