Annotate Android web links without altering addresses containing emoji
This commit is contained in:
1 parent
0a060d04ca
commit
c191b777b7
2 files changed
+35
-26
No files matched your search
+32
-23
@@ -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,
|
||||
|
||||
+3
-3
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user