|
|
|
|
@ -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, |
|
|
|
|
|