Reverses claude new rendering scheme because we need to break into new paragraphs before coming to the rendering stage.
This commit is contained in:
@@ -21,12 +21,10 @@
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -48,32 +46,28 @@ import com.vitorpamplona.amethyst.commons.hashtags.Tunestr
|
||||
import com.vitorpamplona.amethyst.commons.hashtags.Weed
|
||||
import com.vitorpamplona.amethyst.commons.hashtags.Zap
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.richtext.HashTagSegment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RegularTextSegment
|
||||
import com.vitorpamplona.amethyst.ui.components.HashTag
|
||||
import com.vitorpamplona.amethyst.ui.components.RenderRegular
|
||||
import com.vitorpamplona.amethyst.ui.components.RenderTextParagraph
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun RenderHashTagIconsPreview() {
|
||||
val accountViewModel = mockAccountViewModel()
|
||||
ThemeComparisonColumn {
|
||||
RenderRegular(
|
||||
"Testing rendering of hashtags: #flowerstr #Bitcoin, #nostr, #lightning, #zap, #amethyst, #cashu, #plebs, #coffee, #skullofsatoshi, #grownostr, #footstr, #tunestr, #weed, #mate, #gamestr, #gamechain",
|
||||
EmptyTagList,
|
||||
) { paragraph, state, modifier ->
|
||||
RenderTextParagraph(
|
||||
paragraph = paragraph,
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
canPreview = false,
|
||||
quotesLeft = 0,
|
||||
backgroundColor = remember { mutableStateOf(Color.Transparent) },
|
||||
callbackUri = null,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = EmptyNav(),
|
||||
)
|
||||
) { paragraph, state, spaceWidth, modifier ->
|
||||
RenderTextParagraph(paragraph, spaceWidth, modifier) { word ->
|
||||
when (word) {
|
||||
is HashTagSegment -> HashTag(word, EmptyNav())
|
||||
is RegularTextSegment -> Text(word.segmentText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+297
-393
@@ -20,11 +20,14 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.foundation.text.appendInlineContent
|
||||
@@ -47,24 +50,16 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalFontFamilyResolver
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.Placeholder
|
||||
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.TextMeasurer
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.em
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil3.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.compose.produceCachedState
|
||||
import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
@@ -87,6 +82,7 @@ import com.vitorpamplona.amethyst.commons.richtext.RegularTextSegment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextViewerState
|
||||
import com.vitorpamplona.amethyst.commons.richtext.SchemelessUrlSegment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.SecretEmoji
|
||||
import com.vitorpamplona.amethyst.commons.richtext.Segment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.VideoSegment
|
||||
import com.vitorpamplona.amethyst.commons.richtext.WithdrawSegment
|
||||
import com.vitorpamplona.amethyst.model.HashtagIcon
|
||||
@@ -109,6 +105,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.CashuCardBorders
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.inlinePlaceholder
|
||||
import com.vitorpamplona.amethyst.ui.theme.innerPostModifier
|
||||
@@ -153,8 +150,21 @@ fun RenderStrangeNamePreview() {
|
||||
RenderRegular(
|
||||
"If you want to stream or download the music from nostr:npub1sctag667a7np6p6ety2up94pnwwxhd2ep8n8afr2gtr47cwd4ewsvdmmjm can you here",
|
||||
EmptyTagList,
|
||||
) { paragraph, state, modifier ->
|
||||
RenderTextParagraph(paragraph, modifier, state, canPreview = false, quotesLeft = 0, backgroundColor = remember { mutableStateOf(Color.Transparent) }, callbackUri = null, accountViewModel = mockAccountViewModel(), nav = EmptyNav())
|
||||
) { paragraph, state, spaceWidth, modifier ->
|
||||
RenderTextParagraph(paragraph, spaceWidth, modifier) { word ->
|
||||
when (word) {
|
||||
is BechSegment -> {
|
||||
Text(
|
||||
"FreeFrom Official \uD80C\uDD66",
|
||||
modifier = Modifier.border(1.dp, Color.Red),
|
||||
)
|
||||
}
|
||||
|
||||
is RegularTextSegment -> {
|
||||
Text(word.segmentText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,8 +177,51 @@ fun RenderRegularPreview() {
|
||||
"nostr:npub1e0z776cpe0gllgktjk54fuzv8pdfxmq6smsmh8xd7t8s7n474n9smk0txy but i'm Monthly funding" +
|
||||
" 7 other humans vitor@vitorpamplona.com at the moment so spread #test a bit thin, but won't always be the case.",
|
||||
EmptyTagList,
|
||||
) { paragraph, state, modifier ->
|
||||
RenderTextParagraph(paragraph, modifier, state, canPreview = false, quotesLeft = 0, backgroundColor = remember { mutableStateOf(Color.Transparent) }, callbackUri = null, accountViewModel = mockAccountViewModel(), nav = EmptyNav())
|
||||
) { paragraph, state, spaceWidth, modifier ->
|
||||
RenderTextParagraph(paragraph, spaceWidth, modifier) { word ->
|
||||
when (word) {
|
||||
// is ImageSegment -> ZoomableContentView(word.segmentText, state, accountViewModel)
|
||||
// is LinkSegment -> LoadUrlPreview(word.segmentText, word.segmentText, accountViewModel)
|
||||
is EmojiSegment -> {
|
||||
RenderCustomEmoji(word.segmentText, state)
|
||||
}
|
||||
|
||||
// is InvoiceSegment -> MayBeInvoicePreview(word.segmentText)
|
||||
// is WithdrawSegment -> MayBeWithdrawal(word.segmentText)
|
||||
// is CashuSegment -> CashuPreview(word.segmentText, accountViewModel)
|
||||
is EmailSegment -> {
|
||||
ClickableEmail(word.segmentText)
|
||||
}
|
||||
|
||||
is PhoneSegment -> {
|
||||
ClickablePhone(word.segmentText)
|
||||
}
|
||||
|
||||
is BechSegment -> {
|
||||
CreateClickableText(
|
||||
word.segmentText.substring(0, 10),
|
||||
"",
|
||||
1,
|
||||
route = Route.EventRedirect(word.segmentText),
|
||||
nav = EmptyNav(),
|
||||
)
|
||||
}
|
||||
|
||||
is HashTagSegment -> {
|
||||
HashTag(word, EmptyNav())
|
||||
}
|
||||
|
||||
// is HashIndexUserSegment -> TagLink(word, accountViewModel, nav)
|
||||
// is HashIndexEventSegment -> TagLink(word, true, backgroundColorState, accountViewModel, nav)
|
||||
is SchemelessUrlSegment -> {
|
||||
NoProtocolUrlRenderer(word)
|
||||
}
|
||||
|
||||
is RegularTextSegment -> {
|
||||
Text(word.segmentText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,8 +232,30 @@ fun RenderRegularPreview2() {
|
||||
RenderRegular(
|
||||
"#Amethyst v0.84.1: ncryptsec support (NIP-49)",
|
||||
EmptyTagList,
|
||||
) { paragraph, state, modifier ->
|
||||
RenderTextParagraph(paragraph, modifier, state, canPreview = false, quotesLeft = 0, backgroundColor = remember { mutableStateOf(Color.Transparent) }, callbackUri = null, accountViewModel = mockAccountViewModel(), nav = EmptyNav())
|
||||
) { paragraph, state, spaceWidth, modifier ->
|
||||
RenderTextParagraph(paragraph, spaceWidth, modifier) { word ->
|
||||
when (word) {
|
||||
// is ImageSegment -> ZoomableContentView(word.segmentText, state, accountViewModel)
|
||||
// is LinkSegment -> LoadUrlPreview(word.segmentText, word.segmentText, accountViewModel)
|
||||
is EmojiSegment -> RenderCustomEmoji(word.segmentText, state)
|
||||
|
||||
// is InvoiceSegment -> MayBeInvoicePreview(word.segmentText)
|
||||
// is WithdrawSegment -> MayBeWithdrawal(word.segmentText)
|
||||
// is CashuSegment -> CashuPreview(word.segmentText, accountViewModel)
|
||||
is EmailSegment -> ClickableEmail(word.segmentText)
|
||||
|
||||
is PhoneSegment -> ClickablePhone(word.segmentText)
|
||||
|
||||
// is BechSegment -> BechLink(word.segmentText, true, backgroundColor, accountViewModel, nav)
|
||||
is HashTagSegment -> HashTag(word, EmptyNav())
|
||||
|
||||
// is HashIndexUserSegment -> TagLink(word, accountViewModel, nav)
|
||||
// is HashIndexEventSegment -> TagLink(word, true, backgroundColorState, accountViewModel, nav)
|
||||
is SchemelessUrlSegment -> NoProtocolUrlRenderer(word)
|
||||
|
||||
is RegularTextSegment -> Text(word.segmentText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,8 +279,31 @@ fun RenderRegularPreview3() {
|
||||
"#ioメシヨソイゲーム\n" +
|
||||
"https://misskey.io/play/9g3qza4jow",
|
||||
tags,
|
||||
) { paragraph, state, modifier ->
|
||||
RenderTextParagraph(paragraph, modifier, state, canPreview = true, quotesLeft = 1, backgroundColor = remember { mutableStateOf(Color.Transparent) }, callbackUri = null, accountViewModel = accountViewModel, nav = EmptyNav())
|
||||
) { paragraph, state, spaceWidth, modifier ->
|
||||
RenderTextParagraph(paragraph, spaceWidth, modifier) { word ->
|
||||
when (word) {
|
||||
// is ImageSegment -> ZoomableContentView(word.segmentText, state, accountViewModel)
|
||||
is LinkSegment -> LoadUrlPreview(word.segmentText, word.segmentText, null, accountViewModel)
|
||||
|
||||
is EmojiSegment -> RenderCustomEmoji(word.segmentText, state)
|
||||
|
||||
// is InvoiceSegment -> MayBeInvoicePreview(word.segmentText)
|
||||
// is WithdrawSegment -> MayBeWithdrawal(word.segmentText)
|
||||
// is CashuSegment -> CashuPreview(word.segmentText, accountViewModel)
|
||||
is EmailSegment -> ClickableEmail(word.segmentText)
|
||||
|
||||
is PhoneSegment -> ClickablePhone(word.segmentText)
|
||||
|
||||
// is BechSegment -> BechLink(word.segmentText, true, backgroundColor, accountViewModel, nav)
|
||||
is HashTagSegment -> HashTag(word, EmptyNav())
|
||||
|
||||
// is HashIndexUserSegment -> TagLink(word, accountViewModel, nav)
|
||||
// is HashIndexEventSegment -> TagLink(word, true, backgroundColorState, accountViewModel, nav)
|
||||
is SchemelessUrlSegment -> NoProtocolUrlRenderer(word)
|
||||
|
||||
is RegularTextSegment -> Text(word.segmentText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,27 +318,41 @@ private fun RenderRegular(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
RenderRegular(content, tags, callbackUri) { paragraph, state, modifier ->
|
||||
if (paragraph is ImageGalleryParagraph) {
|
||||
ImageGallery(
|
||||
images = paragraph,
|
||||
state = state,
|
||||
accountViewModel = accountViewModel,
|
||||
modifier = modifier,
|
||||
roundedCorner = true,
|
||||
)
|
||||
} else {
|
||||
RenderTextParagraph(
|
||||
paragraph = paragraph,
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
canPreview = canPreview,
|
||||
quotesLeft = quotesLeft,
|
||||
backgroundColor = backgroundColor,
|
||||
callbackUri = callbackUri,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
if (canPreview) {
|
||||
RenderRegular(content, tags, callbackUri) { paragraph, state, spaceWidth, modifier ->
|
||||
if (paragraph is ImageGalleryParagraph) {
|
||||
ImageGallery(
|
||||
images = paragraph,
|
||||
state = state,
|
||||
accountViewModel = accountViewModel,
|
||||
modifier = modifier,
|
||||
roundedCorner = true,
|
||||
)
|
||||
} else {
|
||||
RenderTextParagraph(paragraph, spaceWidth, modifier) { word ->
|
||||
RenderWordWithPreview(
|
||||
word,
|
||||
state,
|
||||
backgroundColor,
|
||||
quotesLeft,
|
||||
callbackUri,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
RenderRegular(content, tags, callbackUri) { paragraph, state, spaceWidth, modifier ->
|
||||
RenderTextParagraph(paragraph, spaceWidth, modifier) { word ->
|
||||
RenderWordWithoutPreview(
|
||||
word,
|
||||
state,
|
||||
backgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -248,352 +360,32 @@ private fun RenderRegular(
|
||||
@Composable
|
||||
fun RenderTextParagraph(
|
||||
paragraph: ParagraphState,
|
||||
spaceWidth: Dp,
|
||||
modifier: Modifier,
|
||||
state: RichTextViewerState,
|
||||
canPreview: Boolean,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
callbackUri: String?,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
renderWord: @Composable (word: Segment) -> Unit,
|
||||
) {
|
||||
val primaryColor = MaterialTheme.colorScheme.primary
|
||||
val onBackgroundColor = MaterialTheme.colorScheme.onBackground
|
||||
val fontSize = LocalTextStyle.current.fontSize
|
||||
|
||||
val emojiSize = if (fontSize == TextUnit.Unspecified) 20.sp else fontSize.times(1.1f)
|
||||
val emojiPlaceholder = Placeholder(emojiSize, emojiSize, PlaceholderVerticalAlign.Center)
|
||||
|
||||
// Fixed-size placeholder for media (images / videos)
|
||||
val mediaPlaceholder = Placeholder(300.sp, 200.sp, PlaceholderVerticalAlign.Bottom)
|
||||
|
||||
// Fixed-size placeholder for block-level widgets (URL previews, invoices, note cards, etc.)
|
||||
val blockPlaceholder = Placeholder(300.sp, 300.sp, PlaceholderVerticalAlign.Bottom)
|
||||
|
||||
// Inline-text-height placeholder for user / event references shown as links
|
||||
val refPlaceholder = Placeholder(150.sp, emojiSize, PlaceholderVerticalAlign.TextCenter)
|
||||
|
||||
// Both the annotated string and the inlineContent map are built in a single pass so
|
||||
// their "inline_$idx" keys always match.
|
||||
val inlineContent = mutableMapOf<String, InlineTextContent>()
|
||||
|
||||
val annotatedText =
|
||||
buildAnnotatedString {
|
||||
paragraph.words.forEachIndexed { idx, word ->
|
||||
if (idx > 0) append(" ")
|
||||
|
||||
when (word) {
|
||||
is RegularTextSegment -> {
|
||||
append(word.segmentText)
|
||||
}
|
||||
|
||||
is EmojiSegment -> {
|
||||
val emojiList =
|
||||
com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji
|
||||
.assembleAnnotatedList(word.segmentText, state.customEmoji)
|
||||
if (emojiList != null) {
|
||||
emojiList.forEachIndexed { emojiIdx, renderable ->
|
||||
when (renderable) {
|
||||
is com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji.TextType -> {
|
||||
append(renderable.text)
|
||||
}
|
||||
|
||||
is com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji.ImageUrlType -> {
|
||||
val key = "inline_${idx}_$emojiIdx"
|
||||
appendInlineContent(key, renderable.url)
|
||||
val url = renderable.url
|
||||
inlineContent[key] =
|
||||
InlineTextContent(emojiPlaceholder) {
|
||||
AsyncImage(
|
||||
model = url,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize().padding(1.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
append(word.segmentText)
|
||||
}
|
||||
}
|
||||
|
||||
is HashTagSegment -> {
|
||||
val hashtagIcon = checkForHashtagWithIcon(word.hashtag)
|
||||
withLink(
|
||||
LinkAnnotation.Clickable(
|
||||
tag = "hashtag_$idx",
|
||||
styles = TextLinkStyles(SpanStyle(color = primaryColor)),
|
||||
) {
|
||||
nav.nav(Route.Hashtag(word.hashtag.lowercase()))
|
||||
},
|
||||
) {
|
||||
append("#${word.hashtag}")
|
||||
if (hashtagIcon != null) {
|
||||
val key = "inline_$idx"
|
||||
appendInlineContent(key, "[icon]")
|
||||
val icon = hashtagIcon
|
||||
inlineContent[key] =
|
||||
InlineTextContent(inlinePlaceholder) {
|
||||
Icon(
|
||||
imageVector = icon.icon,
|
||||
contentDescription = icon.description,
|
||||
tint = Color.Unspecified,
|
||||
modifier = icon.modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
word.extras?.let { withStyle(SpanStyle(color = onBackgroundColor)) { append(it) } }
|
||||
}
|
||||
|
||||
is ImageSegment -> {
|
||||
val key = "inline_$idx"
|
||||
if (canPreview && state.imagesForPager.containsKey(word.segmentText)) {
|
||||
appendInlineContent(key, "[image]")
|
||||
val url = word.segmentText
|
||||
inlineContent[key] =
|
||||
InlineTextContent(mediaPlaceholder) {
|
||||
state.imagesForPager[url]?.let { media ->
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
ZoomableContentView(
|
||||
content = media,
|
||||
images = state.imageList,
|
||||
roundedCorner = true,
|
||||
contentScale = ContentScale.Crop,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
withLink(
|
||||
LinkAnnotation.Url(
|
||||
word.segmentText,
|
||||
TextLinkStyles(SpanStyle(color = primaryColor)),
|
||||
),
|
||||
) { append(word.segmentText) }
|
||||
}
|
||||
}
|
||||
|
||||
is VideoSegment -> {
|
||||
val key = "inline_$idx"
|
||||
if (canPreview && state.imagesForPager.containsKey(word.segmentText)) {
|
||||
appendInlineContent(key, "[video]")
|
||||
val url = word.segmentText
|
||||
inlineContent[key] =
|
||||
InlineTextContent(mediaPlaceholder) {
|
||||
state.imagesForPager[url]?.let { media ->
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
ZoomableContentView(
|
||||
content = media,
|
||||
images = state.imageList,
|
||||
roundedCorner = true,
|
||||
contentScale = ContentScale.Crop,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
withLink(
|
||||
LinkAnnotation.Url(
|
||||
word.segmentText,
|
||||
TextLinkStyles(SpanStyle(color = primaryColor)),
|
||||
),
|
||||
) { append(word.segmentText) }
|
||||
}
|
||||
}
|
||||
|
||||
is Base64Segment -> {
|
||||
val key = "inline_$idx"
|
||||
if (canPreview) {
|
||||
appendInlineContent(key, "[image]")
|
||||
val url = word.segmentText
|
||||
inlineContent[key] =
|
||||
InlineTextContent(mediaPlaceholder) {
|
||||
state.imagesForPager[url]?.let { media ->
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
ZoomableContentView(
|
||||
content = media,
|
||||
images = state.imageList,
|
||||
roundedCorner = true,
|
||||
contentScale = ContentScale.Crop,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
append(word.segmentText)
|
||||
}
|
||||
}
|
||||
|
||||
is LinkSegment -> {
|
||||
val key = "inline_$idx"
|
||||
if (canPreview) {
|
||||
appendInlineContent(key, word.segmentText)
|
||||
val url = word.segmentText
|
||||
inlineContent[key] =
|
||||
InlineTextContent(blockPlaceholder) {
|
||||
LoadUrlPreview(url, url, callbackUri, accountViewModel)
|
||||
}
|
||||
} else {
|
||||
withLink(
|
||||
LinkAnnotation.Url(
|
||||
word.segmentText,
|
||||
TextLinkStyles(SpanStyle(color = primaryColor)),
|
||||
),
|
||||
) { append(word.segmentText) }
|
||||
}
|
||||
}
|
||||
|
||||
is InvoiceSegment -> {
|
||||
if (canPreview) {
|
||||
val key = "inline_$idx"
|
||||
appendInlineContent(key, word.segmentText)
|
||||
val invoice = word.segmentText
|
||||
inlineContent[key] =
|
||||
InlineTextContent(blockPlaceholder) {
|
||||
MayBeInvoicePreview(invoice, accountViewModel)
|
||||
}
|
||||
} else {
|
||||
append(word.segmentText)
|
||||
}
|
||||
}
|
||||
|
||||
is WithdrawSegment -> {
|
||||
if (canPreview) {
|
||||
val key = "inline_$idx"
|
||||
appendInlineContent(key, word.segmentText)
|
||||
val withdraw = word.segmentText
|
||||
inlineContent[key] =
|
||||
InlineTextContent(blockPlaceholder) {
|
||||
MayBeWithdrawal(withdraw, accountViewModel)
|
||||
}
|
||||
} else {
|
||||
append(word.segmentText)
|
||||
}
|
||||
}
|
||||
|
||||
is CashuSegment -> {
|
||||
if (canPreview) {
|
||||
val key = "inline_$idx"
|
||||
appendInlineContent(key, word.segmentText)
|
||||
val cashu = word.segmentText
|
||||
inlineContent[key] =
|
||||
InlineTextContent(blockPlaceholder) {
|
||||
CashuPreview(cashu, accountViewModel)
|
||||
}
|
||||
} else {
|
||||
append(word.segmentText)
|
||||
}
|
||||
}
|
||||
|
||||
is EmailSegment -> {
|
||||
withLink(
|
||||
LinkAnnotation.Url(
|
||||
"mailto:${word.segmentText}",
|
||||
TextLinkStyles(SpanStyle(color = primaryColor)),
|
||||
),
|
||||
) { append(word.segmentText) }
|
||||
}
|
||||
|
||||
is PhoneSegment -> {
|
||||
withLink(
|
||||
LinkAnnotation.Url(
|
||||
"tel:${word.segmentText}",
|
||||
TextLinkStyles(SpanStyle(color = primaryColor)),
|
||||
),
|
||||
) { append(word.segmentText) }
|
||||
}
|
||||
|
||||
is SecretEmoji -> {
|
||||
if (canPreview && quotesLeft > 0) {
|
||||
val key = "inline_$idx"
|
||||
appendInlineContent(key, word.segmentText)
|
||||
val segment = word
|
||||
inlineContent[key] =
|
||||
InlineTextContent(blockPlaceholder) {
|
||||
DisplaySecretEmoji(
|
||||
segment = segment,
|
||||
state = state,
|
||||
callbackUri = callbackUri,
|
||||
canPreview = true,
|
||||
quotesLeft = quotesLeft,
|
||||
backgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
append(word.segmentText)
|
||||
}
|
||||
}
|
||||
|
||||
is BechSegment -> {
|
||||
val key = "inline_$idx"
|
||||
appendInlineContent(key, word.segmentText)
|
||||
val bech = word.segmentText
|
||||
val noteBlockPlaceholder =
|
||||
if (canPreview && quotesLeft > 0) blockPlaceholder else refPlaceholder
|
||||
inlineContent[key] =
|
||||
InlineTextContent(noteBlockPlaceholder) {
|
||||
BechLink(bech, canPreview, quotesLeft, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
is HashIndexUserSegment -> {
|
||||
val key = "inline_$idx"
|
||||
appendInlineContent(key, word.segmentText)
|
||||
val segment = word
|
||||
inlineContent[key] =
|
||||
InlineTextContent(refPlaceholder) {
|
||||
TagLink(segment, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
is HashIndexEventSegment -> {
|
||||
val key = "inline_$idx"
|
||||
appendInlineContent(key, word.segmentText)
|
||||
val segment = word
|
||||
val eventPlaceholder =
|
||||
if (canPreview && quotesLeft > 0) blockPlaceholder else refPlaceholder
|
||||
inlineContent[key] =
|
||||
InlineTextContent(eventPlaceholder) {
|
||||
TagLink(segment, canPreview, quotesLeft, backgroundColor, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
|
||||
is SchemelessUrlSegment -> {
|
||||
withLink(
|
||||
LinkAnnotation.Url(
|
||||
"https://${word.url}",
|
||||
TextLinkStyles(SpanStyle(color = primaryColor)),
|
||||
),
|
||||
) { append(word.url) }
|
||||
word.extras?.let { withStyle(SpanStyle(color = onBackgroundColor)) { append(it) } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = annotatedText,
|
||||
inlineContent = inlineContent,
|
||||
FlowRow(
|
||||
modifier = modifier,
|
||||
)
|
||||
horizontalArrangement = Arrangement.spacedBy(spaceWidth),
|
||||
) {
|
||||
paragraph.words.forEach { word ->
|
||||
renderWord(word)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun RenderRegular(
|
||||
content: String,
|
||||
tags: ImmutableListOfLists<String>,
|
||||
callbackUri: String? = null,
|
||||
renderParagraph: @Composable (ParagraphState, state: RichTextViewerState, modifier: Modifier) -> Unit,
|
||||
renderParagraph: @Composable (ParagraphState, state: RichTextViewerState, Dp, modifier: Modifier) -> Unit,
|
||||
) {
|
||||
val state by remember(content, tags) { mutableStateOf(CachedRichTextParser.parseText(content, tags, callbackUri)) }
|
||||
|
||||
val spaceWidth = measureSpaceWidth(LocalTextStyle.current)
|
||||
|
||||
val currentTextStyle = LocalTextStyle.current
|
||||
|
||||
val textStyle =
|
||||
@@ -618,6 +410,7 @@ fun RenderRegular(
|
||||
renderParagraph(
|
||||
paragraph,
|
||||
state,
|
||||
spaceWidth,
|
||||
Modifier.align(if (paragraph.isRTL) Alignment.End else Alignment.Start),
|
||||
)
|
||||
}
|
||||
@@ -641,6 +434,114 @@ fun measureSpaceWidth(textStyle: TextStyle): Dp {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderWordWithoutPreview(
|
||||
word: Segment,
|
||||
state: RichTextViewerState,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
when (word) {
|
||||
// Don't preview Images
|
||||
is ImageSegment -> ClickableUrl(word.segmentText, word.segmentText)
|
||||
|
||||
// Don't preview Videos
|
||||
is VideoSegment -> ClickableUrl(word.segmentText, word.segmentText)
|
||||
|
||||
is LinkSegment -> ClickableUrl(word.segmentText, word.segmentText)
|
||||
|
||||
is EmojiSegment -> RenderCustomEmoji(word.segmentText, state)
|
||||
|
||||
// Don't offer to pay invoices
|
||||
is InvoiceSegment -> Text(word.segmentText)
|
||||
|
||||
// Don't offer to withdraw
|
||||
is WithdrawSegment -> Text(word.segmentText)
|
||||
|
||||
is CashuSegment -> Text(word.segmentText)
|
||||
|
||||
is EmailSegment -> ClickableEmail(word.segmentText)
|
||||
|
||||
is SecretEmoji -> Text(word.segmentText)
|
||||
|
||||
is PhoneSegment -> ClickablePhone(word.segmentText)
|
||||
|
||||
is BechSegment -> BechLink(word.segmentText, false, 0, backgroundColor, accountViewModel, nav)
|
||||
|
||||
is HashTagSegment -> HashTag(word, nav)
|
||||
|
||||
is HashIndexUserSegment -> TagLink(word, accountViewModel, nav)
|
||||
|
||||
is HashIndexEventSegment -> TagLink(word, false, 0, backgroundColor, accountViewModel, nav)
|
||||
|
||||
is SchemelessUrlSegment -> NoProtocolUrlRenderer(word)
|
||||
|
||||
is RegularTextSegment -> Text(word.segmentText)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderWordWithPreview(
|
||||
word: Segment,
|
||||
state: RichTextViewerState,
|
||||
backgroundColor: MutableState<Color>,
|
||||
quotesLeft: Int,
|
||||
callbackUri: String? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
when (word) {
|
||||
is ImageSegment -> ZoomableContentView(word.segmentText, state, accountViewModel)
|
||||
is VideoSegment -> ZoomableContentView(word.segmentText, state, accountViewModel)
|
||||
is LinkSegment -> LoadUrlPreview(word.segmentText, word.segmentText, callbackUri, accountViewModel)
|
||||
is EmojiSegment -> RenderCustomEmoji(word.segmentText, state)
|
||||
is InvoiceSegment -> MayBeInvoicePreview(word.segmentText, accountViewModel)
|
||||
is WithdrawSegment -> MayBeWithdrawal(word.segmentText, accountViewModel)
|
||||
is CashuSegment -> CashuPreview(word.segmentText, accountViewModel)
|
||||
is EmailSegment -> ClickableEmail(word.segmentText)
|
||||
is SecretEmoji -> DisplaySecretEmoji(word, state, callbackUri, true, quotesLeft, backgroundColor, accountViewModel, nav)
|
||||
is PhoneSegment -> ClickablePhone(word.segmentText)
|
||||
is BechSegment -> BechLink(word.segmentText, true, quotesLeft, backgroundColor, accountViewModel, nav)
|
||||
is HashTagSegment -> HashTag(word, nav)
|
||||
is HashIndexUserSegment -> TagLink(word, accountViewModel, nav)
|
||||
is HashIndexEventSegment -> TagLink(word, true, quotesLeft, backgroundColor, accountViewModel, nav)
|
||||
is SchemelessUrlSegment -> NoProtocolUrlRenderer(word)
|
||||
is RegularTextSegment -> Text(word.segmentText)
|
||||
is Base64Segment -> ZoomableContentView(word.segmentText, state, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ZoomableContentView(
|
||||
word: String,
|
||||
state: RichTextViewerState,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
state.imagesForPager[word]?.let {
|
||||
Box(modifier = HalfVertPadding) {
|
||||
ZoomableContentView(it, state.imageList, roundedCorner = true, contentScale = ContentScale.FillWidth, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NoProtocolUrlRenderer(segment: SchemelessUrlSegment) {
|
||||
ClickableUrl(segment.url, "https://${segment.url}")
|
||||
segment.extras?.let { it1 -> Text(it1) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderCustomEmoji(
|
||||
word: String,
|
||||
state: RichTextViewerState,
|
||||
) {
|
||||
CreateTextWithEmoji(
|
||||
text = word,
|
||||
emojis = state.customEmoji,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BechLink(
|
||||
word: String,
|
||||
@@ -756,6 +657,7 @@ fun DisplaySecretEmoji(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun CoreSecretMessage(
|
||||
localSecretContent: RichTextViewerState,
|
||||
@@ -766,33 +668,35 @@ fun CoreSecretMessage(
|
||||
nav: INav,
|
||||
) {
|
||||
if (localSecretContent.paragraphs.size == 1) {
|
||||
RenderTextParagraph(
|
||||
paragraph = localSecretContent.paragraphs[0],
|
||||
modifier = Modifier,
|
||||
state = localSecretContent,
|
||||
canPreview = true,
|
||||
quotesLeft = quotesLeft,
|
||||
backgroundColor = backgroundColor,
|
||||
callbackUri = callbackUri,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
localSecretContent.paragraphs[0].words.forEach { word ->
|
||||
RenderWordWithPreview(
|
||||
word = word,
|
||||
state = localSecretContent,
|
||||
backgroundColor = backgroundColor,
|
||||
quotesLeft = quotesLeft,
|
||||
callbackUri = callbackUri,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
} else if (localSecretContent.paragraphs.size > 1) {
|
||||
val spaceWidth = measureSpaceWidth(LocalTextStyle.current)
|
||||
|
||||
Column(CashuCardBorders) {
|
||||
localSecretContent.paragraphs.forEach { paragraph ->
|
||||
val modifier = Modifier.align(if (paragraph.isRTL) Alignment.End else Alignment.Start)
|
||||
|
||||
RenderTextParagraph(
|
||||
paragraph = paragraph,
|
||||
modifier = modifier,
|
||||
state = localSecretContent,
|
||||
canPreview = true,
|
||||
quotesLeft = quotesLeft,
|
||||
backgroundColor = backgroundColor,
|
||||
callbackUri = callbackUri,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
RenderTextParagraph(paragraph, spaceWidth, modifier) { word ->
|
||||
RenderWordWithPreview(
|
||||
word,
|
||||
localSecretContent,
|
||||
backgroundColor,
|
||||
quotesLeft,
|
||||
callbackUri,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user