kopia lustrzana https://github.com/vitorpamplona/amethyst
730 wiersze
23 KiB
Kotlin
730 wiersze
23 KiB
Kotlin
/**
|
|
* Copyright (c) 2023 Vitor Pamplona
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
* this software and associated documentation files (the "Software"), to deal in
|
|
* the Software without restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
|
* subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in all
|
|
* copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
*/
|
|
package com.vitorpamplona.amethyst.ui.components
|
|
|
|
import androidx.compose.foundation.gestures.detectTapGestures
|
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
import androidx.compose.foundation.layout.padding
|
|
import androidx.compose.foundation.text.BasicText
|
|
import androidx.compose.foundation.text.ClickableText
|
|
import androidx.compose.foundation.text.InlineTextContent
|
|
import androidx.compose.foundation.text.appendInlineContent
|
|
import androidx.compose.material3.LocalContentColor
|
|
import androidx.compose.material3.LocalTextStyle
|
|
import androidx.compose.material3.MaterialTheme
|
|
import androidx.compose.material3.Text
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.runtime.Immutable
|
|
import androidx.compose.runtime.LaunchedEffect
|
|
import androidx.compose.runtime.derivedStateOf
|
|
import androidx.compose.runtime.getValue
|
|
import androidx.compose.runtime.livedata.observeAsState
|
|
import androidx.compose.runtime.mutableStateOf
|
|
import androidx.compose.runtime.remember
|
|
import androidx.compose.runtime.setValue
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.graphics.Color
|
|
import androidx.compose.ui.graphics.takeOrElse
|
|
import androidx.compose.ui.input.pointer.pointerInput
|
|
import androidx.compose.ui.text.AnnotatedString
|
|
import androidx.compose.ui.text.Placeholder
|
|
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
|
import androidx.compose.ui.text.SpanStyle
|
|
import androidx.compose.ui.text.TextLayoutResult
|
|
import androidx.compose.ui.text.TextStyle
|
|
import androidx.compose.ui.text.buildAnnotatedString
|
|
import androidx.compose.ui.text.font.FontWeight
|
|
import androidx.compose.ui.text.style.TextAlign
|
|
import androidx.compose.ui.text.style.TextOverflow
|
|
import androidx.compose.ui.text.withStyle
|
|
import androidx.compose.ui.unit.TextUnit
|
|
import androidx.compose.ui.unit.dp
|
|
import androidx.compose.ui.unit.sp
|
|
import coil.compose.AsyncImage
|
|
import com.vitorpamplona.amethyst.model.Note
|
|
import com.vitorpamplona.amethyst.model.User
|
|
import com.vitorpamplona.amethyst.service.Nip30CustomEmoji
|
|
import com.vitorpamplona.amethyst.ui.note.LoadChannel
|
|
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
|
import com.vitorpamplona.quartz.encoders.Nip19
|
|
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
|
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
|
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
|
import com.vitorpamplona.quartz.events.toImmutableListOfLists
|
|
import kotlinx.collections.immutable.ImmutableList
|
|
import kotlinx.collections.immutable.ImmutableMap
|
|
import kotlinx.collections.immutable.persistentListOf
|
|
import kotlinx.collections.immutable.toImmutableList
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.launch
|
|
|
|
@Composable
|
|
fun ClickableRoute(
|
|
nip19: Nip19.Return,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit,
|
|
) {
|
|
when (nip19.type) {
|
|
Nip19.Type.USER -> {
|
|
DisplayUser(nip19, accountViewModel, nav)
|
|
}
|
|
Nip19.Type.ADDRESS -> {
|
|
DisplayAddress(nip19, accountViewModel, nav)
|
|
}
|
|
Nip19.Type.NOTE -> {
|
|
DisplayNote(nip19, accountViewModel, nav)
|
|
}
|
|
Nip19.Type.EVENT -> {
|
|
DisplayEvent(nip19, accountViewModel, nav)
|
|
}
|
|
else -> {
|
|
Text(
|
|
remember { "@${nip19.hex}${nip19.additionalChars}" },
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun DisplayEvent(
|
|
nip19: Nip19.Return,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit,
|
|
) {
|
|
LoadNote(nip19.hex, accountViewModel) {
|
|
if (it != null) {
|
|
DisplayNoteLink(it, nip19, accountViewModel, nav)
|
|
} else {
|
|
CreateClickableText(
|
|
clickablePart = remember(nip19) { "@${nip19.hex.toShortenHex()}" },
|
|
suffix = nip19.additionalChars,
|
|
route = remember(nip19) { "Event/${nip19.hex}" },
|
|
nav = nav,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun DisplayNote(
|
|
nip19: Nip19.Return,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit,
|
|
) {
|
|
LoadNote(nip19.hex, accountViewModel = accountViewModel) {
|
|
if (it != null) {
|
|
DisplayNoteLink(it, nip19, accountViewModel, nav)
|
|
} else {
|
|
CreateClickableText(
|
|
clickablePart = remember(nip19) { "@${nip19.hex.toShortenHex()}" },
|
|
suffix = nip19.additionalChars,
|
|
route = remember(nip19) { "Event/${nip19.hex}" },
|
|
nav = nav,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun DisplayNoteLink(
|
|
it: Note,
|
|
nip19: Nip19.Return,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit,
|
|
) {
|
|
val noteState by it.live().metadata.observeAsState()
|
|
|
|
val note = remember(noteState) { noteState?.note } ?: return
|
|
|
|
val channelHex = remember(noteState) { note.channelHex() }
|
|
val noteIdDisplayNote = remember(noteState) { "@${note.idDisplayNote()}" }
|
|
val addedCharts = remember { "${nip19.additionalChars}" }
|
|
|
|
if (note.event is ChannelCreateEvent || nip19.kind == ChannelCreateEvent.KIND) {
|
|
CreateClickableText(
|
|
clickablePart = noteIdDisplayNote,
|
|
suffix = addedCharts,
|
|
route = remember(noteState) { "Channel/${nip19.hex}" },
|
|
nav = nav,
|
|
)
|
|
} else if (note.event is PrivateDmEvent || nip19.kind == PrivateDmEvent.KIND) {
|
|
CreateClickableText(
|
|
clickablePart = noteIdDisplayNote,
|
|
suffix = addedCharts,
|
|
route =
|
|
remember(noteState) { (note.author?.pubkeyHex ?: nip19.hex).let { "RoomByAuthor/$it" } },
|
|
nav = nav,
|
|
)
|
|
} else if (channelHex != null) {
|
|
LoadChannel(baseChannelHex = channelHex, accountViewModel) { baseChannel ->
|
|
val channelState by baseChannel.live.observeAsState()
|
|
val channelDisplayName by
|
|
remember(channelState) {
|
|
derivedStateOf { channelState?.channel?.toBestDisplayName() ?: noteIdDisplayNote }
|
|
}
|
|
|
|
CreateClickableText(
|
|
clickablePart = channelDisplayName,
|
|
suffix = addedCharts,
|
|
route = remember(noteState) { "Channel/${baseChannel.idHex}" },
|
|
nav = nav,
|
|
)
|
|
}
|
|
} else {
|
|
CreateClickableText(
|
|
clickablePart = noteIdDisplayNote,
|
|
suffix = addedCharts,
|
|
route = remember(noteState) { "Event/${nip19.hex}" },
|
|
nav = nav,
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun DisplayAddress(
|
|
nip19: Nip19.Return,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit,
|
|
) {
|
|
var noteBase by remember(nip19) { mutableStateOf(accountViewModel.getNoteIfExists(nip19.hex)) }
|
|
|
|
if (noteBase == null) {
|
|
LaunchedEffect(key1 = nip19.hex) {
|
|
accountViewModel.checkGetOrCreateAddressableNote(nip19.hex) { noteBase = it }
|
|
}
|
|
}
|
|
|
|
noteBase?.let {
|
|
val noteState by it.live().metadata.observeAsState()
|
|
|
|
val route = remember(noteState) { "Note/${nip19.hex}" }
|
|
val displayName = remember(noteState) { "@${noteState?.note?.idDisplayNote()}" }
|
|
val addedCharts = remember { "${nip19.additionalChars}" }
|
|
|
|
CreateClickableText(
|
|
clickablePart = displayName,
|
|
suffix = addedCharts,
|
|
route = route,
|
|
nav = nav,
|
|
)
|
|
}
|
|
|
|
if (noteBase == null) {
|
|
Text(
|
|
remember { "@${nip19.hex}${nip19.additionalChars}" },
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun DisplayUser(
|
|
nip19: Nip19.Return,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit,
|
|
) {
|
|
var userBase by
|
|
remember(nip19) {
|
|
mutableStateOf(
|
|
accountViewModel.getUserIfExists(nip19.hex),
|
|
)
|
|
}
|
|
|
|
if (userBase == null) {
|
|
LaunchedEffect(key1 = nip19.hex) {
|
|
accountViewModel.checkGetOrCreateUser(nip19.hex) { userBase = it }
|
|
}
|
|
}
|
|
|
|
userBase?.let { RenderUserAsClickableText(it, nip19, nav) }
|
|
|
|
if (userBase == null) {
|
|
Text(
|
|
remember { "@${nip19.hex}${nip19.additionalChars}" },
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun RenderUserAsClickableText(
|
|
baseUser: User,
|
|
nip19: Nip19.Return,
|
|
nav: (String) -> Unit,
|
|
) {
|
|
val userState by baseUser.live().metadata.observeAsState()
|
|
val route = remember { "User/${baseUser.pubkeyHex}" }
|
|
|
|
val userDisplayName by
|
|
remember(userState) { derivedStateOf { userState?.user?.toBestDisplayName() } }
|
|
|
|
val userTags by
|
|
remember(userState) {
|
|
derivedStateOf { userState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists() }
|
|
}
|
|
|
|
val addedCharts = remember(nip19) { "${nip19.additionalChars}" }
|
|
|
|
userDisplayName?.let {
|
|
CreateClickableTextWithEmoji(
|
|
clickablePart = it,
|
|
suffix = addedCharts,
|
|
maxLines = 1,
|
|
route = route,
|
|
nav = nav,
|
|
tags = userTags,
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun CreateClickableText(
|
|
clickablePart: String,
|
|
suffix: String?,
|
|
maxLines: Int = Int.MAX_VALUE,
|
|
overrideColor: Color? = null,
|
|
fontWeight: FontWeight = FontWeight.Normal,
|
|
route: String,
|
|
nav: (String) -> Unit,
|
|
) {
|
|
val currentStyle = LocalTextStyle.current
|
|
val primaryColor = MaterialTheme.colorScheme.primary
|
|
val onBackgroundColor = MaterialTheme.colorScheme.onBackground
|
|
|
|
val clickablePartStyle =
|
|
remember(primaryColor, overrideColor) {
|
|
currentStyle
|
|
.copy(color = overrideColor ?: primaryColor, fontWeight = fontWeight)
|
|
.toSpanStyle()
|
|
}
|
|
|
|
val nonClickablePartStyle =
|
|
remember(onBackgroundColor, overrideColor) {
|
|
currentStyle
|
|
.copy(color = overrideColor ?: onBackgroundColor, fontWeight = fontWeight)
|
|
.toSpanStyle()
|
|
}
|
|
|
|
val text =
|
|
remember(clickablePartStyle, nonClickablePartStyle, clickablePart, suffix) {
|
|
buildAnnotatedString {
|
|
withStyle(clickablePartStyle) { append(clickablePart) }
|
|
if (!suffix.isNullOrBlank()) {
|
|
withStyle(nonClickablePartStyle) { append(suffix) }
|
|
}
|
|
}
|
|
}
|
|
|
|
ClickableText(
|
|
text = text,
|
|
maxLines = maxLines,
|
|
onClick = { nav(route) },
|
|
)
|
|
}
|
|
|
|
@Composable
|
|
fun CreateTextWithEmoji(
|
|
text: String,
|
|
tags: ImmutableListOfLists<String>?,
|
|
color: Color = Color.Unspecified,
|
|
textAlign: TextAlign? = null,
|
|
fontWeight: FontWeight? = null,
|
|
fontSize: TextUnit = TextUnit.Unspecified,
|
|
maxLines: Int = Int.MAX_VALUE,
|
|
overflow: TextOverflow = TextOverflow.Clip,
|
|
modifier: Modifier = Modifier,
|
|
) {
|
|
var emojiList by remember(text) { mutableStateOf<ImmutableList<Renderable>>(persistentListOf()) }
|
|
|
|
LaunchedEffect(key1 = text) {
|
|
launch(Dispatchers.Default) {
|
|
val emojis =
|
|
tags?.lists?.filter { it.size > 2 && it[0] == "emoji" }?.associate { ":${it[1]}:" to it[2] }
|
|
?: emptyMap()
|
|
|
|
if (emojis.isNotEmpty()) {
|
|
val newEmojiList = assembleAnnotatedList(text, emojis)
|
|
if (newEmojiList.isNotEmpty()) {
|
|
emojiList = newEmojiList.toImmutableList()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
val textColor =
|
|
color.takeOrElse { LocalTextStyle.current.color.takeOrElse { LocalContentColor.current } }
|
|
|
|
if (emojiList.isEmpty()) {
|
|
Text(
|
|
text = text,
|
|
color = textColor,
|
|
textAlign = textAlign,
|
|
fontWeight = fontWeight,
|
|
fontSize = fontSize,
|
|
maxLines = maxLines,
|
|
overflow = overflow,
|
|
modifier = modifier,
|
|
)
|
|
} else {
|
|
val style =
|
|
LocalTextStyle.current
|
|
.merge(
|
|
TextStyle(
|
|
color = textColor,
|
|
textAlign = TextAlign.Unspecified,
|
|
fontWeight = fontWeight,
|
|
fontSize = fontSize,
|
|
),
|
|
)
|
|
.toSpanStyle()
|
|
|
|
InLineIconRenderer(emojiList, style, fontSize, maxLines, overflow, modifier)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun CreateTextWithEmoji(
|
|
text: String,
|
|
emojis: ImmutableMap<String, String>,
|
|
color: Color = Color.Unspecified,
|
|
textAlign: TextAlign? = null,
|
|
fontWeight: FontWeight? = null,
|
|
fontSize: TextUnit = TextUnit.Unspecified,
|
|
maxLines: Int = Int.MAX_VALUE,
|
|
overflow: TextOverflow = TextOverflow.Clip,
|
|
modifier: Modifier = Modifier,
|
|
) {
|
|
var emojiList by remember(text) { mutableStateOf<ImmutableList<Renderable>>(persistentListOf()) }
|
|
|
|
if (emojis.isNotEmpty()) {
|
|
LaunchedEffect(key1 = text) {
|
|
launch(Dispatchers.Default) {
|
|
val newEmojiList = assembleAnnotatedList(text, emojis)
|
|
if (newEmojiList.isNotEmpty()) {
|
|
emojiList = newEmojiList.toImmutableList()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
val textColor =
|
|
color.takeOrElse { LocalTextStyle.current.color.takeOrElse { LocalContentColor.current } }
|
|
|
|
if (emojiList.isEmpty()) {
|
|
Text(
|
|
text = text,
|
|
color = textColor,
|
|
textAlign = textAlign,
|
|
fontWeight = fontWeight,
|
|
fontSize = fontSize,
|
|
maxLines = maxLines,
|
|
overflow = overflow,
|
|
modifier = modifier,
|
|
)
|
|
} else {
|
|
val currentStyle = LocalTextStyle.current
|
|
val style =
|
|
remember(currentStyle) {
|
|
currentStyle
|
|
.merge(
|
|
TextStyle(
|
|
color = textColor,
|
|
textAlign = TextAlign.Unspecified,
|
|
fontWeight = fontWeight,
|
|
fontSize = fontSize,
|
|
),
|
|
)
|
|
.toSpanStyle()
|
|
}
|
|
|
|
InLineIconRenderer(emojiList, style, fontSize, maxLines, overflow, modifier)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun CreateClickableTextWithEmoji(
|
|
clickablePart: String,
|
|
maxLines: Int = Int.MAX_VALUE,
|
|
tags: ImmutableListOfLists<String>?,
|
|
style: TextStyle,
|
|
onClick: (Int) -> Unit,
|
|
) {
|
|
var emojiList by
|
|
remember(clickablePart) { mutableStateOf<ImmutableList<Renderable>>(persistentListOf()) }
|
|
|
|
LaunchedEffect(key1 = clickablePart) {
|
|
launch(Dispatchers.Default) {
|
|
val emojis =
|
|
tags?.lists?.filter { it.size > 2 && it[0] == "emoji" }?.associate { ":${it[1]}:" to it[2] }
|
|
?: emptyMap()
|
|
|
|
if (emojis.isNotEmpty()) {
|
|
val newEmojiList = assembleAnnotatedList(clickablePart, emojis)
|
|
if (newEmojiList.isNotEmpty()) {
|
|
emojiList = newEmojiList.toImmutableList()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (emojiList.isEmpty()) {
|
|
ClickableText(
|
|
text = AnnotatedString(clickablePart),
|
|
style = style,
|
|
maxLines = maxLines,
|
|
onClick = onClick,
|
|
)
|
|
} else {
|
|
ClickableInLineIconRenderer(emojiList, maxLines, style.toSpanStyle()) { onClick(it) }
|
|
}
|
|
}
|
|
|
|
@Immutable
|
|
data class DoubleEmojiList(
|
|
val part1: ImmutableList<Renderable>,
|
|
val part2: ImmutableList<Renderable>,
|
|
)
|
|
|
|
@Composable
|
|
fun CreateClickableTextWithEmoji(
|
|
clickablePart: String,
|
|
suffix: String?,
|
|
maxLines: Int = Int.MAX_VALUE,
|
|
overrideColor: Color? = null,
|
|
fontWeight: FontWeight = FontWeight.Normal,
|
|
route: String,
|
|
nav: (String) -> Unit,
|
|
tags: ImmutableListOfLists<String>?,
|
|
) {
|
|
var emojiLists by remember(clickablePart) { mutableStateOf<DoubleEmojiList?>(null) }
|
|
|
|
LaunchedEffect(key1 = clickablePart) {
|
|
launch(Dispatchers.Default) {
|
|
val emojis =
|
|
tags?.lists?.filter { it.size > 2 && it[0] == "emoji" }?.associate { ":${it[1]}:" to it[2] }
|
|
?: emptyMap()
|
|
|
|
if (emojis.isNotEmpty()) {
|
|
val newEmojiList1 = assembleAnnotatedList(clickablePart, emojis)
|
|
val newEmojiList2 =
|
|
suffix?.let { assembleAnnotatedList(it, emojis) } ?: emptyList<Renderable>()
|
|
|
|
if (newEmojiList1.isNotEmpty() || newEmojiList2.isNotEmpty()) {
|
|
emojiLists =
|
|
DoubleEmojiList(newEmojiList1.toImmutableList(), newEmojiList2.toImmutableList())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (emojiLists == null) {
|
|
CreateClickableText(clickablePart, suffix, maxLines, overrideColor, fontWeight, route, nav)
|
|
} else {
|
|
ClickableInLineIconRenderer(
|
|
emojiLists!!.part1,
|
|
maxLines,
|
|
LocalTextStyle.current
|
|
.copy(color = overrideColor ?: MaterialTheme.colorScheme.primary, fontWeight = fontWeight)
|
|
.toSpanStyle(),
|
|
) {
|
|
nav(route)
|
|
}
|
|
|
|
InLineIconRenderer(
|
|
emojiLists!!.part2,
|
|
LocalTextStyle.current
|
|
.copy(
|
|
color = overrideColor ?: MaterialTheme.colorScheme.onBackground,
|
|
fontWeight = fontWeight,
|
|
)
|
|
.toSpanStyle(),
|
|
maxLines = maxLines,
|
|
)
|
|
}
|
|
}
|
|
|
|
suspend fun assembleAnnotatedList(
|
|
text: String,
|
|
emojis: Map<String, String>,
|
|
): ImmutableList<Renderable> {
|
|
return Nip30CustomEmoji()
|
|
.buildArray(text)
|
|
.map {
|
|
val url = emojis[it]
|
|
if (url != null) {
|
|
ImageUrlType(url)
|
|
} else {
|
|
TextType(it)
|
|
}
|
|
}
|
|
.toImmutableList()
|
|
}
|
|
|
|
@Immutable open class Renderable()
|
|
|
|
@Immutable class TextType(val text: String) : Renderable()
|
|
|
|
@Immutable class ImageUrlType(val url: String) : Renderable()
|
|
|
|
@Composable
|
|
fun ClickableInLineIconRenderer(
|
|
wordsInOrder: ImmutableList<Renderable>,
|
|
maxLines: Int = Int.MAX_VALUE,
|
|
style: SpanStyle,
|
|
onClick: (Int) -> Unit,
|
|
) {
|
|
val placeholderSize =
|
|
remember(style) {
|
|
if (style.fontSize == TextUnit.Unspecified) {
|
|
22.sp
|
|
} else {
|
|
style.fontSize.times(1.1f)
|
|
}
|
|
}
|
|
|
|
val inlineContent =
|
|
wordsInOrder
|
|
.mapIndexedNotNull { idx, value ->
|
|
if (value is ImageUrlType) {
|
|
Pair(
|
|
"inlineContent$idx",
|
|
InlineTextContent(
|
|
Placeholder(
|
|
width = placeholderSize,
|
|
height = placeholderSize,
|
|
placeholderVerticalAlign = PlaceholderVerticalAlign.Center,
|
|
),
|
|
) {
|
|
AsyncImage(
|
|
model = value.url,
|
|
contentDescription = null,
|
|
modifier = Modifier.fillMaxSize().padding(1.dp),
|
|
)
|
|
},
|
|
)
|
|
} else {
|
|
null
|
|
}
|
|
}
|
|
.associate { it.first to it.second }
|
|
|
|
val annotatedText =
|
|
buildAnnotatedString {
|
|
wordsInOrder.forEachIndexed { idx, value ->
|
|
withStyle(
|
|
style,
|
|
) {
|
|
if (value is TextType) {
|
|
append(value.text)
|
|
} else if (value is ImageUrlType) {
|
|
appendInlineContent("inlineContent$idx", "[icon]")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
val layoutResult = remember { mutableStateOf<TextLayoutResult?>(null) }
|
|
val pressIndicator =
|
|
Modifier.pointerInput(onClick) {
|
|
detectTapGestures { pos ->
|
|
layoutResult.value?.let { layoutResult -> onClick(layoutResult.getOffsetForPosition(pos)) }
|
|
}
|
|
}
|
|
|
|
BasicText(
|
|
text = annotatedText,
|
|
modifier = pressIndicator,
|
|
inlineContent = inlineContent,
|
|
maxLines = maxLines,
|
|
onTextLayout = { layoutResult.value = it },
|
|
)
|
|
}
|
|
|
|
@Composable
|
|
fun InLineIconRenderer(
|
|
wordsInOrder: ImmutableList<Renderable>,
|
|
style: SpanStyle,
|
|
fontSize: TextUnit = TextUnit.Unspecified,
|
|
maxLines: Int = Int.MAX_VALUE,
|
|
overflow: TextOverflow = TextOverflow.Clip,
|
|
modifier: Modifier = Modifier,
|
|
) {
|
|
val placeholderSize =
|
|
remember(fontSize) {
|
|
if (fontSize == TextUnit.Unspecified) {
|
|
22.sp
|
|
} else {
|
|
fontSize.times(1.1f)
|
|
}
|
|
}
|
|
|
|
val inlineContent =
|
|
wordsInOrder
|
|
.mapIndexedNotNull { idx, value ->
|
|
if (value is ImageUrlType) {
|
|
Pair(
|
|
"inlineContent$idx",
|
|
InlineTextContent(
|
|
Placeholder(
|
|
width = placeholderSize,
|
|
height = placeholderSize,
|
|
placeholderVerticalAlign = PlaceholderVerticalAlign.Center,
|
|
),
|
|
) {
|
|
AsyncImage(
|
|
model = value.url,
|
|
contentDescription = null,
|
|
modifier = Modifier.fillMaxSize().padding(horizontal = 0.dp),
|
|
)
|
|
},
|
|
)
|
|
} else {
|
|
null
|
|
}
|
|
}
|
|
.associate { it.first to it.second }
|
|
|
|
val annotatedText =
|
|
remember {
|
|
buildAnnotatedString {
|
|
wordsInOrder.forEachIndexed { idx, value ->
|
|
withStyle(
|
|
style,
|
|
) {
|
|
if (value is TextType) {
|
|
append(value.text)
|
|
} else if (value is ImageUrlType) {
|
|
appendInlineContent("inlineContent$idx", "[icon]")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Text(
|
|
text = annotatedText,
|
|
inlineContent = inlineContent,
|
|
fontSize = fontSize,
|
|
maxLines = maxLines,
|
|
overflow = overflow,
|
|
modifier = modifier,
|
|
)
|
|
}
|