refactor(richtext): RichTextViewer moves to commonsUI over a platform slot

The shared rich-text core already asked a RichTextSegmentRenderer for
every platform segment; what kept the viewer in the app was how a caller
reached the app's renderer. That is now a slot:
- RichTextPlatform (commonsUI): builds the segment renderer for a viewer
  call and renders markdown. LocalRichTextPlatform defaults to Plain,
  which renders those segments as text.
- InlineQuoteRenderer and LocalInlineQuoteRenderer move to commonsUI with
  a plain default (a link to the quoted note).
- The app provides AndroidRichTextPlatform (its existing
  AmethystRichTextSegmentRenderer and markdown renderer) and its
  quoted-note card in AmethystTheme, which wraps every Activity root.
  Chat bubbles still override the quote renderer locally.

RichTextViewer (formerly delegating to CommonsBackedRichTextViewer, now
folded into it), LoadNote, BechLink, TagLink, HashTag, the paragraph
layout and ClickableRoute move to commonsUI commons.ui.components. The
platform leaves (Blossom URI renderers, the zoomable-media wrapper, the
legacy segment switchboards and the secret-emoji message that uses them)
stay in the app in RichTextPlatformLeaves.kt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P
This commit is contained in:
Claude
2026-09-28 17:41:15 +00:00
parent e06f27ea4f
commit 3fc3cc6311
56 changed files with 763 additions and 572 deletions
@@ -80,6 +80,7 @@ import com.vitorpamplona.amethyst.commons.resources.lightning_invoice
import com.vitorpamplona.amethyst.commons.resources.what_s_on_your_mind
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog
import com.vitorpamplona.amethyst.commons.ui.components.BechLink
import com.vitorpamplona.amethyst.commons.ui.components.OutlinedThinPaddingTextField
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
@@ -100,7 +101,6 @@ import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.service.playback.composable.VideoView
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
import com.vitorpamplona.amethyst.ui.components.BechLink
import com.vitorpamplona.amethyst.ui.components.LoadUrlPreview
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest
@@ -21,22 +21,17 @@
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
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.layout.ContentScale
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.richtext.BlossomUriSegment
import com.vitorpamplona.amethyst.commons.richtext.BuzzInviteLinkSegment
import com.vitorpamplona.amethyst.commons.richtext.CachedRichTextParser
import com.vitorpamplona.amethyst.commons.richtext.CashuSegment
import com.vitorpamplona.amethyst.commons.richtext.ClinkOfferSegment
import com.vitorpamplona.amethyst.commons.richtext.ConcordInviteLinkSegment
@@ -52,6 +47,7 @@ import com.vitorpamplona.amethyst.commons.richtext.RichTextViewerState
import com.vitorpamplona.amethyst.commons.richtext.SecretEmoji
import com.vitorpamplona.amethyst.commons.richtext.Segment
import com.vitorpamplona.amethyst.commons.richtext.WithdrawSegment
import com.vitorpamplona.amethyst.commons.ui.components.BechLink
import com.vitorpamplona.amethyst.commons.ui.components.ClickableBuzzInviteLink
import com.vitorpamplona.amethyst.commons.ui.components.ClickableConcordInviteLink
import com.vitorpamplona.amethyst.commons.ui.components.ClickableEmail
@@ -59,17 +55,16 @@ import com.vitorpamplona.amethyst.commons.ui.components.ClickablePhone
import com.vitorpamplona.amethyst.commons.ui.components.ClickableRelayGroupLink
import com.vitorpamplona.amethyst.commons.ui.components.ClickableRelayUrl
import com.vitorpamplona.amethyst.commons.ui.components.NowhereLinkCard
import com.vitorpamplona.amethyst.commons.ui.components.RichTextViewer
import com.vitorpamplona.amethyst.commons.ui.components.TagLink
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.richtext.LocalRichTextInteractions
import com.vitorpamplona.amethyst.commons.ui.richtext.LocalRichTextSegmentRenderer
import com.vitorpamplona.amethyst.commons.ui.richtext.RichTextInteractions
import com.vitorpamplona.amethyst.commons.ui.richtext.RichTextPlatform
import com.vitorpamplona.amethyst.commons.ui.richtext.RichTextSegmentRenderer
import com.vitorpamplona.amethyst.commons.ui.theme.HalfVertPadding
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.components.markdown.RenderContentAsMarkdown
import com.vitorpamplona.amethyst.ui.note.creators.invoice.ClinkOfferPreview
import com.vitorpamplona.amethyst.ui.note.creators.invoice.MayBeInvoicePreview
import com.vitorpamplona.amethyst.commons.ui.richtext.RichTextViewer as CommonsRichTextViewer
/**
* The Android implementation of the shared [RichTextSegmentRenderer] contract: it
@@ -79,7 +74,7 @@ import com.vitorpamplona.amethyst.commons.ui.richtext.RichTextViewer as CommonsR
* leaves need but that the cross-platform contract deliberately keeps out of its
* method signatures.
*
* Recreate one per [CommonsBackedRichTextViewer] call (it captures per-call state);
* Recreate one per [RichTextViewer] call (it captures per-call state);
* it is cheap and [remember]ed against its inputs so the CompositionLocal doesn't
* churn its readers.
*/
@@ -255,63 +250,27 @@ class AmethystRichTextSegmentRenderer(
}
/**
* Renders rich text through the shared cross-platform [CommonsRichTextViewer] core.
* This is the production plain-text path that [RichTextViewer] delegates to: it
* parses with the existing [CachedRichTextParser], keeps the markdown path native,
* and for plain rich text provides the Android segment renderer
* ([AmethystRichTextSegmentRenderer]) plus the universal interactions into the two
* CompositionLocals the core reads.
*
* The core renders text, custom emoji, and hashtags (with their shared inline
* icons) itself; every divergent segment routes back to Amethyst's existing
* leaves through the renderer. Note: url/email/phone open via the platform URI
* handler rather than Amethyst's per-type Clickable* composables, so their tap
* behavior is the generic open action (rendering is unchanged clickable text).
* Android's [RichTextPlatform]: its segment renderer ([AmethystRichTextSegmentRenderer]) and its
* markdown renderer. Installed in the app theme, so every screen's rich text gets them.
*/
@Composable
fun CommonsBackedRichTextViewer(
content: String,
canPreview: Boolean,
quotesLeft: Int,
modifier: Modifier,
tags: ImmutableListOfLists<String>,
backgroundColor: MutableState<Color>,
callbackUri: String? = null,
authorPubKey: String? = null,
accountViewModel: AccountViewModel,
nav: INav,
) {
Column(modifier = modifier) {
if (remember(content) { CachedRichTextParser.isMarkdown(content) }) {
RenderContentAsMarkdown(content, tags, canPreview, quotesLeft, backgroundColor, callbackUri, accountViewModel, nav)
return@Column
}
object AndroidRichTextPlatform : RichTextPlatform {
override fun segmentRenderer(
accountViewModel: AccountViewModel,
nav: INav,
backgroundColor: MutableState<Color>,
callbackUri: String?,
canPreview: Boolean,
): RichTextSegmentRenderer = AmethystRichTextSegmentRenderer(accountViewModel, nav, backgroundColor, callbackUri, canPreview)
val state by remember(content, tags) {
mutableStateOf(CachedRichTextParser.parseText(content, tags, callbackUri, authorPubKey))
}
val renderer =
remember(accountViewModel, nav, backgroundColor, callbackUri, canPreview) {
AmethystRichTextSegmentRenderer(accountViewModel, nav, backgroundColor, callbackUri, canPreview)
}
val interactions =
remember(nav) {
RichTextInteractions(
onClickHashtag = { nav.nav(Route.Hashtag(it.lowercase())) },
)
}
CompositionLocalProvider(
LocalRichTextSegmentRenderer provides renderer,
LocalRichTextInteractions provides interactions,
) {
CommonsRichTextViewer(
state = state,
canPreview = canPreview,
quotesLeft = quotesLeft,
)
}
}
@Composable
override fun Markdown(
content: String,
tags: ImmutableListOfLists<String>,
canPreview: Boolean,
quotesLeft: Int,
backgroundColor: MutableState<Color>,
callbackUri: String?,
accountViewModel: AccountViewModel,
nav: INav,
) = RenderContentAsMarkdown(content, tags, canPreview, quotesLeft, backgroundColor, callbackUri, accountViewModel, nav)
}
@@ -43,6 +43,7 @@ import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.show_more
import com.vitorpamplona.amethyst.commons.richtext.ExpandableTextCutOffCalculator
import com.vitorpamplona.amethyst.commons.ui.components.RichTextViewer
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder
@@ -21,44 +21,12 @@
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.components.InlineQuoteRenderer
import com.vitorpamplona.amethyst.commons.ui.theme.innerPostModifier
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.note.types.ReplyRenderType
/**
* Strategy for rendering a note quoted inline in another note's text (a
* `nostr:nevent1...`/`nostr:note1...` mention with preview budget left, or a
* legacy `#[index]` event tag).
*
* Both rich-text paths (plain and markdown) funnel through [DisplayFullNote],
* which reads [LocalInlineQuoteRenderer], so providing a different renderer
* re-skins inline quotes for a whole subtree without threading a parameter
* through the parser stack. Chat bubbles use this to keep a quoted chat
* message in the chat reply design instead of the default quoted-note card
* (see `chatInlineQuoteRenderer`).
*
* A renderer draws only the quoted note itself; any leftover characters
* around the mention stay with the caller ([DisplayFullNote]).
*/
fun interface InlineQuoteRenderer {
@Composable
fun Render(
note: Note,
quotesLeft: Int,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: INav,
)
}
/** The quoted-note card used everywhere outside chats. */
/** The quoted-note card used everywhere outside chats; the app installs it in its theme root. */
val DefaultInlineQuoteRenderer =
InlineQuoteRenderer { note, quotesLeft, backgroundColor, accountViewModel, nav ->
NoteCompose(
@@ -72,5 +40,3 @@ val DefaultInlineQuoteRenderer =
nav = nav,
)
}
val LocalInlineQuoteRenderer = compositionLocalOf { DefaultInlineQuoteRenderer }
@@ -22,21 +22,13 @@ 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.padding
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
@@ -48,32 +40,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFontFamilyResolver
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextMeasurer
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextOverflow
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.dp
import androidx.compose.ui.unit.em
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderFilterAssemblerSubscription
import com.vitorpamplona.amethyst.commons.relayClient.user.observeUserDisplayNickname
import com.vitorpamplona.amethyst.commons.relayClient.user.observeUserInfo
import com.vitorpamplona.amethyst.commons.richtext.Base64Segment
import com.vitorpamplona.amethyst.commons.richtext.BechSegment
import com.vitorpamplona.amethyst.commons.richtext.BlossomUriSegment
@@ -92,7 +66,6 @@ import com.vitorpamplona.amethyst.commons.richtext.InvoiceSegment
import com.vitorpamplona.amethyst.commons.richtext.LinkSegment
import com.vitorpamplona.amethyst.commons.richtext.MathSegment
import com.vitorpamplona.amethyst.commons.richtext.NowhereLinkSegment
import com.vitorpamplona.amethyst.commons.richtext.ParagraphState
import com.vitorpamplona.amethyst.commons.richtext.PdfSegment
import com.vitorpamplona.amethyst.commons.richtext.PhoneSegment
import com.vitorpamplona.amethyst.commons.richtext.RegularTextSegment
@@ -105,6 +78,7 @@ 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.commons.ui.components.AnimatedBorderTextCornerRadius
import com.vitorpamplona.amethyst.commons.ui.components.BechLink
import com.vitorpamplona.amethyst.commons.ui.components.ClickableBuzzInviteLink
import com.vitorpamplona.amethyst.commons.ui.components.ClickableConcordInviteLink
import com.vitorpamplona.amethyst.commons.ui.components.ClickableEmail
@@ -112,63 +86,27 @@ import com.vitorpamplona.amethyst.commons.ui.components.ClickablePhone
import com.vitorpamplona.amethyst.commons.ui.components.ClickableRelayGroupLink
import com.vitorpamplona.amethyst.commons.ui.components.ClickableRelayUrl
import com.vitorpamplona.amethyst.commons.ui.components.ClickableTextPrimary
import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.commons.ui.components.CreateClickableText
import com.vitorpamplona.amethyst.commons.ui.components.HashTag
import com.vitorpamplona.amethyst.commons.ui.components.NowhereLinkCard
import com.vitorpamplona.amethyst.commons.ui.components.RenderCustomEmoji
import com.vitorpamplona.amethyst.commons.ui.components.RenderRegular
import com.vitorpamplona.amethyst.commons.ui.components.RenderTextParagraph
import com.vitorpamplona.amethyst.commons.ui.components.TagLink
import com.vitorpamplona.amethyst.commons.ui.components.measureSpaceWidth
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.commons.ui.note.LoadUser
import com.vitorpamplona.amethyst.commons.ui.richtext.CreateClickableTextWithEmoji
import com.vitorpamplona.amethyst.commons.ui.richtext.CreateTextWithEmoji
import com.vitorpamplona.amethyst.commons.ui.richtext.HashtagIcon
import com.vitorpamplona.amethyst.commons.ui.richtext.checkForHashtagWithIcon
import com.vitorpamplona.amethyst.commons.ui.state.produceCachedState
import com.vitorpamplona.amethyst.commons.ui.theme.CashuCardBorders
import com.vitorpamplona.amethyst.commons.ui.theme.HalfVertPadding
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.commons.ui.theme.inlinePlaceholder
import com.vitorpamplona.amethyst.commons.util.toShortDisplay
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.service.uploads.blossom.bud10.openBlossomUriAsIntent
import com.vitorpamplona.amethyst.ui.note.creators.invoice.ClinkOfferPreview
import com.vitorpamplona.amethyst.ui.note.creators.invoice.MayBeInvoicePreview
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nipB7Blossom.BlossomUri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
/**
* Renders Nostr rich text. The plain (non-markdown) path now runs through the
* cross-platform [com.vitorpamplona.amethyst.commons.ui.richtext.RichTextViewer]
* core via [CommonsBackedRichTextViewer]; this entry is kept so its ~55 call sites
* are untouched. The Android-specific markdown path stays here.
*/
@Composable
fun RichTextViewer(
content: String,
canPreview: Boolean,
quotesLeft: Int,
modifier: Modifier,
tags: ImmutableListOfLists<String>,
backgroundColor: MutableState<Color>,
callbackUri: String? = null,
authorPubKey: String? = null,
accountViewModel: AccountViewModel,
nav: INav,
) = CommonsBackedRichTextViewer(
content = content,
canPreview = canPreview,
quotesLeft = quotesLeft,
modifier = modifier,
tags = tags,
backgroundColor = backgroundColor,
callbackUri = callbackUri,
authorPubKey = authorPubKey,
accountViewModel = accountViewModel,
nav = nav,
)
@Preview
@Composable
fun RenderStrangeNamePreview() {
@@ -356,88 +294,6 @@ fun RenderRegularPreview3() {
}
}
@Composable
fun RenderTextParagraph(
paragraph: ParagraphState,
spaceWidth: Dp,
modifier: Modifier,
renderWord: @Composable (word: Segment) -> Unit,
) {
FlowRow(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(spaceWidth),
// Center items on the cross axis so a taller item (an equation, whose image
// is taller than a text line) sits centered on the line instead of hanging
// below the baseline. No-op for the common all-text row.
itemVerticalAlignment = Alignment.CenterVertically,
) {
paragraph.words.forEach { word ->
renderWord(word)
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun RenderRegular(
content: String,
tags: ImmutableListOfLists<String>,
callbackUri: String? = null,
authorPubKey: String? = null,
renderParagraph: @Composable (ParagraphState, state: RichTextViewerState, Dp, modifier: Modifier) -> Unit,
) {
val state by remember(content, tags) { mutableStateOf(CachedRichTextParser.parseText(content, tags, callbackUri, authorPubKey)) }
val spaceWidth = measureSpaceWidth(LocalTextStyle.current)
val currentTextStyle = LocalTextStyle.current
val textStyle =
remember(currentTextStyle) {
currentTextStyle.copy(
lineHeight = 1.3.em,
)
}
Column {
// FlowRow doesn't work well with paragraphs. So we need to split them
state.paragraphs.forEach { paragraph ->
CompositionLocalProvider(
LocalLayoutDirection provides
if (paragraph.isRTL) {
LayoutDirection.Rtl
} else {
LayoutDirection.Ltr
},
LocalTextStyle provides textStyle,
) {
renderParagraph(
paragraph,
state,
spaceWidth,
Modifier.align(if (paragraph.isRTL) Alignment.End else Alignment.Start),
)
}
}
}
}
@Composable
fun measureSpaceWidth(textStyle: TextStyle): Dp {
val fontFamilyResolver = LocalFontFamilyResolver.current
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
return remember(fontFamilyResolver, density, layoutDirection, textStyle) {
val widthPx =
TextMeasurer(fontFamilyResolver, density, layoutDirection, 1)
.measure(" ", textStyle)
.size
.width
with(density) { widthPx.toDp() }
}
}
@Composable
private fun RenderWordWithoutPreview(
word: Segment,
@@ -640,76 +496,6 @@ private fun NoProtocolUrlRenderer(url: String) {
ClickableUrl(url, "https://$url")
}
@Composable
fun RenderCustomEmoji(
word: String,
state: RichTextViewerState,
) {
CreateTextWithEmoji(
text = word,
emojis = state.customEmoji,
)
}
@Composable
fun BechLink(
word: String,
canPreview: Boolean,
quotesLeft: Int,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) {
val loadedLink by produceCachedState(cache = accountViewModel.bechLinkCache, key = word)
val baseNote = loadedLink?.baseNote
// Preload the placeholder's author NIP-65 outbox so a missing quoted event can be fetched from there.
baseNote?.author?.let { author ->
UserFinderFilterAssemblerSubscription(author, accountViewModel)
}
if (canPreview && quotesLeft > 0 && baseNote != null) {
Row {
DisplayFullNote(
note = baseNote,
extraChars = loadedLink?.nip19?.additionalChars?.ifBlank { null },
quotesLeft = quotesLeft,
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav,
)
}
} else if (loadedLink?.nip19 != null) {
ClickableRoute(word, loadedLink?.nip19!!, accountViewModel, nav)
} else {
val text =
remember(word) {
if (word.length > 16) {
word.replaceRange(8, word.length - 8, ":")
} else {
word
}
}
Text(text = text, maxLines = 1)
}
}
@Composable
fun DisplayFullNote(
note: Note,
extraChars: String?,
quotesLeft: Int,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) {
LocalInlineQuoteRenderer.current.Render(note, quotesLeft, backgroundColor, accountViewModel, nav)
extraChars?.let { Text(it) }
}
@Composable
fun DisplaySecretEmoji(
segment: SecretEmoji,
@@ -803,197 +589,3 @@ fun CoreSecretMessage(
}
}
}
@Composable
fun HashTag(
segment: HashTagSegment,
nav: INav,
) {
val primary = MaterialTheme.colorScheme.primary
val background = MaterialTheme.colorScheme.onBackground
val hashtagIcon: HashtagIcon? = checkForHashtagWithIcon(segment.hashtag)
val annotatedTermsString =
remember(segment.segmentText) {
buildAnnotatedString {
withStyle(SpanStyle(color = primary)) {
pushStringAnnotation("routeToHashtag", "")
append("#${segment.hashtag}")
pop()
}
if (hashtagIcon != null) {
withStyle(SpanStyle(color = primary)) {
pushStringAnnotation("routeToHashtag", "")
appendInlineContent("inlineContent", "[icon]")
pop()
}
}
segment.extras?.let { withStyle(SpanStyle(color = background)) { append(it) } }
}
}
Text(
text = annotatedTermsString,
modifier =
remember {
Modifier.clickable {
nav.nav(Route.Hashtag(segment.hashtag.lowercase()))
}
},
inlineContent =
if (hashtagIcon != null) {
mapOf("inlineContent" to inlineIcon(hashtagIcon))
} else {
emptyMap()
},
)
}
@Composable
private fun inlineIcon(hashtagIcon: HashtagIcon) =
InlineTextContent(inlinePlaceholder) {
Icon(
imageVector = hashtagIcon.icon,
contentDescription = hashtagIcon.description,
tint = Color.Unspecified,
modifier = hashtagIcon.modifier,
)
}
@Composable
fun TagLink(
word: HashIndexUserSegment,
accountViewModel: AccountViewModel,
nav: INav,
) {
LoadUser(baseUserHex = word.hex) {
if (it == null) {
Text(text = word.segmentText)
} else {
Row {
DisplayUserFromTag(it, accountViewModel, nav)
word.extras?.let { it2 ->
Text(text = it2)
}
}
}
}
}
@Composable
fun LoadNote(
baseNoteHex: String,
content: @Composable (Note?) -> Unit,
) {
var note by
remember(baseNoteHex) { mutableStateOf(LocalCache.getNoteIfExists(baseNoteHex)) }
if (note == null) {
LaunchedEffect(key1 = baseNoteHex) {
note = LocalCache.checkGetOrCreateNote(baseNoteHex)
}
}
content(note)
}
@Composable
fun TagLink(
word: HashIndexEventSegment,
canPreview: Boolean,
quotesLeft: Int,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) {
LoadNote(baseNoteHex = word.hex) {
if (it == null) {
Text(text = remember { word.segmentText.toShortDisplay() })
} else {
Row {
DisplayNoteFromTag(
it,
word.extras,
canPreview,
quotesLeft,
accountViewModel,
backgroundColor,
nav,
)
}
}
}
}
@Preview
@Composable
fun DisplayNoteFromTagPreview() {
val dummyPost =
TextNoteEvent(
id = "0b6d941c46411a95edb1c93da7ad6ca26370497d8c7b7d621f5cb59f48841bad",
pubKey = "6dd3b72e325da7383b275eef1c66131ba4664326e162bc060527509b4e33ae43",
createdAt = 1753988264,
tags = emptyArray(),
content = "test",
sig = "ec39e60722a083cccbd2d82d2827e13f5499fa7cbcedac5b76011a844c077473adb629d50d01fab147835ac6c8a3d5ba9aaddd87d6723f0c3c864b9119fc4356",
)
LocalCache.justConsume(dummyPost, null, true)
val note = LocalCache.getOrCreateNote(dummyPost.id)
ThemeComparisonColumn(
toPreview = {
ClickableTextPrimary(
text = "@${note.idNote().toShortDisplay()}",
onClick = { },
)
},
)
}
@Composable
private fun DisplayNoteFromTag(
baseNote: Note,
addedChars: String?,
canPreview: Boolean,
quotesLeft: Int,
accountViewModel: AccountViewModel,
backgroundColor: MutableState<Color>,
nav: INav,
) {
if (canPreview && quotesLeft > 0) {
LocalInlineQuoteRenderer.current.Render(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
} else {
ClickableTextPrimary(
text = "@${baseNote.idNote().toShortDisplay()}",
onClick = { routeFor(baseNote, accountViewModel.account)?.let { nav.nav(it) } },
)
}
addedChars?.ifBlank { null }?.let { Text(text = it) }
}
@Composable
private fun DisplayUserFromTag(
baseUser: User,
accountViewModel: AccountViewModel,
nav: INav,
) {
val meta by observeUserInfo(baseUser, accountViewModel)
val nickname by observeUserDisplayNickname(baseUser, accountViewModel)
val petName = nickname?.petName
CrossfadeIfEnabled(targetState = meta, label = "DisplayUserFromTag") {
Row {
CreateClickableTextWithEmoji(
clickablePart = remember(meta, petName) { petName ?: it?.info?.bestName() ?: baseUser.pubkeyDisplayHex() },
maxLines = 1,
route = remember(baseUser) { routeFor(baseUser) },
nav = nav,
tags = if (petName != null) nickname?.tags else it?.tags,
)
}
}
}
@@ -36,6 +36,8 @@ import com.halilibo.richtext.ui.string.RichTextString
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.commons.ui.components.DisplayFullNote
import com.vitorpamplona.amethyst.commons.ui.components.DisplayUser
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.richtext.HashtagIcon
import com.vitorpamplona.amethyst.commons.ui.richtext.checkForHashtagWithIcon
@@ -44,8 +46,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size17Modifier
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.LoadedBechLink
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.components.DisplayFullNote
import com.vitorpamplona.amethyst.ui.components.DisplayUser
import com.vitorpamplona.amethyst.ui.components.LoadUrlPreview
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
import com.vitorpamplona.amethyst.ui.fragmentHashtagOrNull
@@ -61,6 +61,7 @@ import com.vitorpamplona.amethyst.commons.resources.nip05_failed
import com.vitorpamplona.amethyst.commons.resources.nip05_verified
import com.vitorpamplona.amethyst.commons.ui.components.ClickableTextPrimary
import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote
@@ -78,7 +79,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.nip05
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.painterRes
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.tags.aTag.firstTaggedAddress
@@ -40,6 +40,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.user.observeUserInfo
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.and
import com.vitorpamplona.amethyst.commons.resources.replying_to
import com.vitorpamplona.amethyst.commons.ui.components.CreateClickableTextWithEmoji
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.commons.ui.richtext.CreateClickableTextWithEmoji
@@ -48,7 +49,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.commons.ui.theme.lessImportantLink
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@@ -30,13 +30,13 @@ import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.relayClient.event.observeNote
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.forked_from
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.commons.ui.note.QuietMark
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.quartz.experimental.forks.IForkableEvent
@Composable
@@ -90,6 +90,7 @@ import com.vitorpamplona.amethyst.commons.resources.share_as_image
import com.vitorpamplona.amethyst.commons.resources.share_as_image_generating
import com.vitorpamplona.amethyst.commons.resources.share_as_image_url
import com.vitorpamplona.amethyst.commons.resources.share_as_image_watermark
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner
import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
@@ -101,7 +102,6 @@ import com.vitorpamplona.amethyst.service.uploads.CompressorQuality
import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator
import com.vitorpamplona.amethyst.service.uploads.UploadingState
import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@@ -58,11 +58,11 @@ import com.vitorpamplona.amethyst.commons.resources.share_as_qr_hint_nostr
import com.vitorpamplona.amethyst.commons.resources.share_as_qr_hint_web
import com.vitorpamplona.amethyst.commons.resources.share_as_qr_mode_nostr
import com.vitorpamplona.amethyst.commons.resources.share_as_qr_mode_web
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.KeepScreenBrightAndAwake
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.QrCodeDrawer
@@ -69,6 +69,7 @@ import com.vitorpamplona.amethyst.commons.resources.attestation_valid_to
import com.vitorpamplona.amethyst.commons.resources.attestor_proficiency
import com.vitorpamplona.amethyst.commons.resources.attestor_recommendation
import com.vitorpamplona.amethyst.commons.resources.attestor_recommendation_for_kinds
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote
@@ -78,7 +79,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.note.UserCompose
@@ -78,6 +78,7 @@ import com.vitorpamplona.amethyst.commons.resources.preview_card_image_for
import com.vitorpamplona.amethyst.commons.resources.profile_image
import com.vitorpamplona.amethyst.commons.resources.quick_action_share
import com.vitorpamplona.amethyst.commons.resources.quick_action_share_browser_link
import com.vitorpamplona.amethyst.commons.ui.components.RichTextViewer
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor
@@ -102,7 +103,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.innerPostModifier
import com.vitorpamplona.amethyst.commons.util.equalImmutableLists
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
import com.vitorpamplona.amethyst.ui.components.RichTextViewer
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.note.LikeReaction
import com.vitorpamplona.amethyst.ui.note.ZapReaction
@@ -64,6 +64,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.user.observeUserInfo
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.preview_card_image_for
import com.vitorpamplona.amethyst.commons.resources.rating_stars_out_of_five
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote
@@ -75,7 +76,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp
import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.quartz.experimental.publications.PublicationIndexEvent
@@ -32,8 +32,8 @@ import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent
import com.vitorpamplona.amethyst.commons.richtext.MediaLocalImage
import com.vitorpamplona.amethyst.commons.richtext.MediaLocalVideo
import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent
@@ -54,6 +54,9 @@ import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
import com.vitorpamplona.amethyst.commons.relayClient.event.observeNote
import com.vitorpamplona.amethyst.commons.relayClient.user.observeUserInfo
import com.vitorpamplona.amethyst.commons.ui.components.ClickableTextPrimary
import com.vitorpamplona.amethyst.commons.ui.components.CreateClickableTextWithEmoji
import com.vitorpamplona.amethyst.commons.ui.components.RenderUserAsClickableText
import com.vitorpamplona.amethyst.commons.ui.components.measureSpaceWidth
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor
@@ -64,10 +67,7 @@ import com.vitorpamplona.amethyst.commons.ui.richtext.CreateClickableTextWithEmo
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.RenderUserAsClickableText
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.components.measureSpaceWidth
import com.vitorpamplona.amethyst.ui.components.rememberTranslation
import com.vitorpamplona.amethyst.ui.note.nip22Comments.DisplayExternalId
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
@@ -73,6 +73,7 @@ import com.vitorpamplona.amethyst.commons.resources.music_playlist_private
import com.vitorpamplona.amethyst.commons.resources.music_playlist_track_count
import com.vitorpamplona.amethyst.commons.resources.music_playlist_unknown_track
import com.vitorpamplona.amethyst.commons.resources.preview_card_image_for
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.BaseUserPicture
@@ -86,7 +87,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageBanner
@@ -61,6 +61,7 @@ import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
import com.vitorpamplona.amethyst.commons.music.formatTrackDuration
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.preview_card_image_for
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.stringRes
@@ -76,7 +77,6 @@ import com.vitorpamplona.amethyst.service.playback.composable.PauseControllerWhe
import com.vitorpamplona.amethyst.service.playback.composable.VideoView
import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.GetMediaItem
import com.vitorpamplona.amethyst.service.playback.composable.wavefront.syntheticWaveformFor
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader
@@ -35,11 +35,11 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
import com.vitorpamplona.amethyst.commons.ui.components.RenderUserAsClickableText
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.theme.HalfHalfTopPadding
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.components.RenderUserAsClickableText
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags
@@ -63,6 +63,7 @@ import com.vitorpamplona.amethyst.commons.resources.publication_contents
import com.vitorpamplona.amethyst.commons.resources.publication_more_sections
import com.vitorpamplona.amethyst.commons.resources.publication_section_count
import com.vitorpamplona.amethyst.commons.resources.publication_untitled_section
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
@@ -70,7 +71,6 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.quartz.experimental.library.BlossomPieceIndexEvent
@@ -52,6 +52,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteEvent
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.publication_untitled_section
import com.vitorpamplona.amethyst.commons.richtext.CachedAsciiDocToMarkdown
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote
@@ -60,7 +61,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp
import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.components.markdown.RenderContentAsMarkdown
import com.vitorpamplona.quartz.experimental.publications.PublicationContentEvent
import com.vitorpamplona.quartz.experimental.publications.PublicationIndexEvent
@@ -48,6 +48,7 @@ import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.video_curation_set_private_items
import com.vitorpamplona.amethyst.commons.resources.video_curation_set_video_count
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
@@ -55,7 +56,6 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.components.DisplayBlurHash
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark
@@ -22,9 +22,9 @@ package com.vitorpamplona.amethyst.ui.note.types
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.quartz.cyberspace.deck0003Sno.SnoPalette
import com.vitorpamplona.quartz.cyberspace.deck0003Sno.SnoPaletteEventReader
import com.vitorpamplona.quartz.cyberspace.deck0003Sno.SnoPaletteRef
@@ -52,6 +52,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.accept_the_suggestion
import com.vitorpamplona.amethyst.commons.resources.proposal_to_edit
import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.commons.ui.stringRes
@@ -61,7 +62,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier
import com.vitorpamplona.amethyst.commons.ui.theme.innerPostModifier
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.actions.EditPostView
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.note.NoteBody
import com.vitorpamplona.amethyst.ui.note.observeEdits
@@ -51,6 +51,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteEvent
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.torrent_file
import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor
@@ -60,7 +61,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier
import com.vitorpamplona.amethyst.commons.util.countToHumanReadableBytes
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent
import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent
@@ -46,13 +46,13 @@ import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteEvent
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp
import com.vitorpamplona.amethyst.commons.ui.theme.grayText
import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent
@@ -34,12 +34,12 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.commons.ui.note.LoadUser
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.note.UserCompose
import com.vitorpamplona.quartz.nip01Core.core.Address
@@ -29,10 +29,10 @@ import androidx.compose.ui.graphics.Color
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.kind_mute_list
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.EventTag
@@ -131,6 +131,7 @@ import com.vitorpamplona.amethyst.commons.resources.backup_review_unmuted
import com.vitorpamplona.amethyst.commons.resources.backup_review_unmuted_explainer
import com.vitorpamplona.amethyst.commons.resources.backup_review_was_named
import com.vitorpamplona.amethyst.commons.resources.backup_review_words_and_hashtags
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor
@@ -142,7 +143,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.amethyst.commons.util.toShortDisplay
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.note.elements.BannerImage
@@ -87,6 +87,7 @@ import com.vitorpamplona.amethyst.commons.resources.backup_conflict_restore_mine
import com.vitorpamplona.amethyst.commons.resources.backup_review_changed_by_other_app
import com.vitorpamplona.amethyst.commons.resources.backup_review_title_cleared
import com.vitorpamplona.amethyst.commons.resources.backup_review_title_updated
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor
@@ -102,7 +103,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.amethyst.commons.util.toShortDisplay
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListDiff
@@ -42,13 +42,13 @@ import com.vitorpamplona.amethyst.commons.nip51Lists.ui.BookmarkGroupManagementI
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.bookmarks_title
import com.vitorpamplona.amethyst.commons.resources.post_bookmark_management_title
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.NewListButton
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark
@@ -21,8 +21,8 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.ui.components.InlineQuoteRenderer
import com.vitorpamplona.amethyst.ui.components.DefaultInlineQuoteRenderer
import com.vitorpamplona.amethyst.ui.components.InlineQuoteRenderer
import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
@@ -53,12 +53,12 @@ import com.vitorpamplona.amethyst.commons.model.latestBuzzEdit
import com.vitorpamplona.amethyst.commons.model.latestConcordEdit
import com.vitorpamplona.amethyst.commons.model.latestMarmotEdit
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.ui.components.LocalInlineQuoteRenderer
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.commons.ui.theme.ReactionRowZapraiser
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.components.LocalInlineQuoteRenderer
import com.vitorpamplona.amethyst.ui.note.RenderZapRaiser
import com.vitorpamplona.amethyst.ui.note.WatchBlockAndReport
import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent
@@ -51,6 +51,7 @@ import com.vitorpamplona.amethyst.commons.resources.channel_image
import com.vitorpamplona.amethyst.commons.resources.created_at
import com.vitorpamplona.amethyst.commons.resources.groups_no_descriptor
import com.vitorpamplona.amethyst.commons.resources.owner
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.NoteAuthorPicture
@@ -65,7 +66,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.commons.ui.theme.largeProfilePictureModifier
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.LinkChatButton
@@ -42,6 +42,7 @@ import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChann
import com.vitorpamplona.amethyst.commons.relayClient.user.observeUserIsFollowingChannel
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.profile_image
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings
@@ -52,7 +53,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp
import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.note.LikeReaction
import com.vitorpamplona.amethyst.ui.note.ZapReaction
@@ -39,13 +39,13 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.commons.ui.theme.Size20dp
import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.note.ZapReaction
import com.vitorpamplona.amethyst.ui.note.types.GoalProgressBar
import com.vitorpamplona.quartz.nip75ZapGoals.ZapGoalEvent
@@ -30,12 +30,12 @@ import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.new_community_note
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.Size26Modifier
import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.painterRes
@Composable
@@ -39,12 +39,12 @@ import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.contact_list_screen_title
import com.vitorpamplona.amethyst.commons.resources.contact_list_screen_title_with_count
import com.vitorpamplona.amethyst.commons.resources.fetching_follow_list
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.note.UserCompose
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import kotlinx.collections.immutable.ImmutableList
@@ -66,6 +66,7 @@ import com.vitorpamplona.amethyst.commons.resources.nwc_payment_request
import com.vitorpamplona.amethyst.commons.resources.wallet_connect_pay_invoice_error_error
import com.vitorpamplona.amethyst.commons.service.nwc.nwcFailureDetail
import com.vitorpamplona.amethyst.commons.service.nwc.nwcTimeoutMessage
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedEmpty
import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold
@@ -77,7 +78,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SimpleImage75Modifier
import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
import com.vitorpamplona.amethyst.ui.components.ReusableZapButton
import com.vitorpamplona.amethyst.ui.components.ZapButtonConfig
@@ -30,13 +30,13 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextOverflow
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.relayClient.event.observeNoteAndMap
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.MyExtensibleTopAppBar
import com.vitorpamplona.amethyst.commons.ui.note.ArrowBackIcon
import com.vitorpamplona.amethyst.commons.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.commons.ui.theme.SimpleImage35Modifier
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
import com.vitorpamplona.amethyst.ui.note.elements.BannerImage
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
@@ -70,12 +70,12 @@ import com.vitorpamplona.amethyst.commons.resources.git_repo_code_loading
import com.vitorpamplona.amethyst.commons.resources.git_repo_copy_file
import com.vitorpamplona.amethyst.commons.resources.git_repo_file_load_error
import com.vitorpamplona.amethyst.commons.resources.git_repo_plain_text
import com.vitorpamplona.amethyst.commons.ui.components.RichTextViewer
import com.vitorpamplona.amethyst.commons.ui.components.util.setText
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.components.RichTextViewer
import com.vitorpamplona.quartz.nip34Git.git.GitRepoSnapshot
import com.vitorpamplona.quartz.nip34Git.git.GitTreeEntry
import kotlinx.coroutines.Dispatchers
@@ -40,10 +40,10 @@ import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.git_repo_code_loading
import com.vitorpamplona.amethyst.commons.resources.git_repo_file_load_error
import com.vitorpamplona.amethyst.commons.resources.git_repo_readme_missing
import com.vitorpamplona.amethyst.commons.ui.components.RichTextViewer
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.components.RichTextViewer
import com.vitorpamplona.quartz.nip34Git.git.GitRepoSnapshot
import com.vitorpamplona.quartz.nip34Git.git.GitTreeEntry
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
@@ -51,13 +51,13 @@ import com.vitorpamplona.amethyst.commons.resources.dvm_home_status_processing
import com.vitorpamplona.amethyst.commons.resources.dvm_home_status_requesting
import com.vitorpamplona.amethyst.commons.resources.dvm_home_status_requesting_all
import com.vitorpamplona.amethyst.commons.resources.dvm_offline_banner
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.DvmPaymentActions
import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.rememberDvmHeartbeatFresh
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
@@ -24,9 +24,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.components.LoadNote
@Composable
fun ShowDonationCard(
@@ -66,6 +66,7 @@ import com.vitorpamplona.amethyst.commons.resources.this_version_brought_to_you_
import com.vitorpamplona.amethyst.commons.resources.version_name
import com.vitorpamplona.amethyst.commons.resources.zap_the_devs_description
import com.vitorpamplona.amethyst.commons.resources.zap_the_devs_title
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.components.appendLink
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
@@ -77,7 +78,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.components.ReusableZapButton
import com.vitorpamplona.amethyst.ui.components.ZapButtonConfig
import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.DisplayZapSplits
@@ -34,9 +34,9 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
@@ -94,6 +94,7 @@ import com.vitorpamplona.amethyst.commons.resources.poll_results_selections
import com.vitorpamplona.amethyst.commons.resources.poll_results_title
import com.vitorpamplona.amethyst.commons.resources.poll_results_your_pick
import com.vitorpamplona.amethyst.commons.resources.poll_single_choice
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton
@@ -118,7 +119,6 @@ import com.vitorpamplona.amethyst.commons.viewmodels.nip88Polls.PollOptionResult
import com.vitorpamplona.amethyst.commons.viewmodels.nip88Polls.PollResultsUiState
import com.vitorpamplona.amethyst.commons.viewmodels.nip88Polls.PollResultsViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.nip88Polls.PollVoterRow
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserLine
import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton
import com.vitorpamplona.amethyst.ui.note.types.UserGallery
@@ -30,11 +30,11 @@ import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.relayClient.event.observeNote
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.note.CheckHiddenFeedWatchBlockAndReport
import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent
@@ -43,13 +43,13 @@ import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.looking_for_event
import com.vitorpamplona.amethyst.commons.resources.looking_for_event_title
import com.vitorpamplona.amethyst.commons.resources.looking_for_private_event
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -94,6 +94,7 @@ import com.vitorpamplona.amethyst.commons.resources.send_the_seller_a_message
import com.vitorpamplona.amethyst.commons.resources.thread_collapsed_reply_count
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav
@@ -139,7 +140,6 @@ import com.vitorpamplona.amethyst.commons.util.showAmount
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.thread.LevelFeedViewModel
import com.vitorpamplona.amethyst.ui.components.AutoNonlazyGrid
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
import com.vitorpamplona.amethyst.ui.note.CheckAndDisplayEditStatus
@@ -25,6 +25,7 @@ import androidx.compose.runtime.Composable
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.thread_title
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
@@ -32,7 +33,6 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensible
import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal.ThreadFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.ThreadFilterAssemblerSubscription
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.theme
import android.app.Activity
import androidx.compose.material3.ColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
@@ -38,6 +39,8 @@ import com.vitorpamplona.amethyst.commons.model.AccentColorType
import com.vitorpamplona.amethyst.commons.model.FontFamilyType
import com.vitorpamplona.amethyst.commons.model.FontSizeType
import com.vitorpamplona.amethyst.commons.model.ThemeType
import com.vitorpamplona.amethyst.commons.ui.components.LocalInlineQuoteRenderer
import com.vitorpamplona.amethyst.commons.ui.richtext.LocalRichTextPlatform
import com.vitorpamplona.amethyst.commons.ui.screen.DisplaySettings
import com.vitorpamplona.amethyst.commons.ui.screen.collectDisplaySettings
import com.vitorpamplona.amethyst.commons.ui.theme.AmethystMaterialTheme
@@ -46,6 +49,8 @@ import com.vitorpamplona.amethyst.commons.ui.theme.amethystLightColors
import com.vitorpamplona.amethyst.commons.ui.theme.isDarkTheme
import com.vitorpamplona.amethyst.commons.ui.theme.isLight
import com.vitorpamplona.amethyst.commons.ui.theme.transparentBackground
import com.vitorpamplona.amethyst.ui.components.AndroidRichTextPlatform
import com.vitorpamplona.amethyst.ui.components.DefaultInlineQuoteRenderer
val chartLightColors =
VicoTheme(
@@ -120,8 +125,15 @@ fun AmethystTheme(
// ImageLoaderSetup registers the avatar thumbnail cache and the local Blossom bridge.
profilePictureCache = true,
colors = colors,
content = content,
)
) {
// The platform halves of the shared note and rich-text renderers: media, LaTeX, payments,
// link previews, and the quoted-note card. Every Activity root goes through this theme.
CompositionLocalProvider(
LocalRichTextPlatform provides AndroidRichTextPlatform,
LocalInlineQuoteRenderer provides DefaultInlineQuoteRenderer,
content = content,
)
}
val view = LocalView.current
if (!view.isInEditMode) {
@@ -18,7 +18,7 @@
* 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
package com.vitorpamplona.amethyst.commons.ui.components
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -0,0 +1,71 @@
/*
* Copyright (c) 2025 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.commons.ui.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.commons.util.toShortDisplay
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
/**
* Strategy for rendering a note quoted inline in another note's text (a
* `nostr:nevent1...`/`nostr:note1...` mention with preview budget left, or a
* legacy `#[index]` event tag).
*
* Both rich-text paths (plain and markdown) funnel through [DisplayFullNote],
* which reads [LocalInlineQuoteRenderer], so providing a different renderer
* re-skins inline quotes for a whole subtree without threading a parameter
* through the parser stack. Chat bubbles use this to keep a quoted chat
* message in the chat reply design instead of the default quoted-note card
* (see `chatInlineQuoteRenderer`).
*
* A renderer draws only the quoted note itself; any leftover characters
* around the mention stay with the caller ([DisplayFullNote]).
*/
fun interface InlineQuoteRenderer {
@Composable
fun Render(
note: Note,
quotesLeft: Int,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: INav,
)
}
/**
* A link to the quoted note, for front ends that install no card renderer of their own (Android
* installs its quoted-note card in its theme root).
*/
val PlainInlineQuoteRenderer =
InlineQuoteRenderer { note, _, _, accountViewModel, nav ->
ClickableTextPrimary(
text = "@${note.idNote().toShortDisplay()}",
onClick = { routeFor(note, accountViewModel.account)?.let { nav.nav(it) } },
)
}
val LocalInlineQuoteRenderer = compositionLocalOf { PlainInlineQuoteRenderer }
@@ -0,0 +1,498 @@
/*
* Copyright (c) 2025 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.commons.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
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.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFontFamilyResolver
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextMeasurer
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
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.em
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderFilterAssemblerSubscription
import com.vitorpamplona.amethyst.commons.relayClient.user.observeUserDisplayNickname
import com.vitorpamplona.amethyst.commons.relayClient.user.observeUserInfo
import com.vitorpamplona.amethyst.commons.richtext.CachedRichTextParser
import com.vitorpamplona.amethyst.commons.richtext.HashIndexEventSegment
import com.vitorpamplona.amethyst.commons.richtext.HashIndexUserSegment
import com.vitorpamplona.amethyst.commons.richtext.HashTagSegment
import com.vitorpamplona.amethyst.commons.richtext.ParagraphState
import com.vitorpamplona.amethyst.commons.richtext.RichTextViewerState
import com.vitorpamplona.amethyst.commons.richtext.Segment
import com.vitorpamplona.amethyst.commons.ui.components.ClickableTextPrimary
import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.commons.ui.components.LocalInlineQuoteRenderer
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.commons.ui.note.LoadUser
import com.vitorpamplona.amethyst.commons.ui.richtext.CreateClickableTextWithEmoji
import com.vitorpamplona.amethyst.commons.ui.richtext.CreateTextWithEmoji
import com.vitorpamplona.amethyst.commons.ui.richtext.HashtagIcon
import com.vitorpamplona.amethyst.commons.ui.richtext.LocalRichTextInteractions
import com.vitorpamplona.amethyst.commons.ui.richtext.LocalRichTextPlatform
import com.vitorpamplona.amethyst.commons.ui.richtext.LocalRichTextSegmentRenderer
import com.vitorpamplona.amethyst.commons.ui.richtext.RichTextInteractions
import com.vitorpamplona.amethyst.commons.ui.richtext.RichTextSegmentRenderer
import com.vitorpamplona.amethyst.commons.ui.richtext.checkForHashtagWithIcon
import com.vitorpamplona.amethyst.commons.ui.state.produceCachedState
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.commons.ui.theme.inlinePlaceholder
import com.vitorpamplona.amethyst.commons.util.toShortDisplay
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.amethyst.commons.ui.richtext.RichTextViewer as CommonsRichTextViewer
/**
* Renders Nostr rich text. Markdown goes to the front end's renderer; plain rich text is parsed
* once ([CachedRichTextParser]) and laid out by the shared [CommonsRichTextViewer] core, which
* renders text, emoji, hashtags and mentions itself and hands every platform segment (media,
* LaTeX, payments, link previews) to the [RichTextSegmentRenderer] the front end's
* [LocalRichTextPlatform] builds for this call.
*/
@Composable
fun RichTextViewer(
content: String,
canPreview: Boolean,
quotesLeft: Int,
modifier: Modifier,
tags: ImmutableListOfLists<String>,
backgroundColor: MutableState<Color>,
callbackUri: String? = null,
authorPubKey: String? = null,
accountViewModel: AccountViewModel,
nav: INav,
) {
val platform = LocalRichTextPlatform.current
Column(modifier = modifier) {
if (remember(content) { CachedRichTextParser.isMarkdown(content) }) {
platform.Markdown(content, tags, canPreview, quotesLeft, backgroundColor, callbackUri, accountViewModel, nav)
return@Column
}
val state by remember(content, tags) {
mutableStateOf(CachedRichTextParser.parseText(content, tags, callbackUri, authorPubKey))
}
val renderer =
remember(platform, accountViewModel, nav, backgroundColor, callbackUri, canPreview) {
platform.segmentRenderer(accountViewModel, nav, backgroundColor, callbackUri, canPreview)
}
val interactions =
remember(nav) {
RichTextInteractions(
onClickHashtag = { nav.nav(Route.Hashtag(it.lowercase())) },
)
}
CompositionLocalProvider(
LocalRichTextSegmentRenderer provides renderer,
LocalRichTextInteractions provides interactions,
) {
CommonsRichTextViewer(
state = state,
canPreview = canPreview,
quotesLeft = quotesLeft,
)
}
}
}
@Composable
fun RenderTextParagraph(
paragraph: ParagraphState,
spaceWidth: Dp,
modifier: Modifier,
renderWord: @Composable (word: Segment) -> Unit,
) {
FlowRow(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(spaceWidth),
// Center items on the cross axis so a taller item (an equation, whose image
// is taller than a text line) sits centered on the line instead of hanging
// below the baseline. No-op for the common all-text row.
itemVerticalAlignment = Alignment.CenterVertically,
) {
paragraph.words.forEach { word ->
renderWord(word)
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun RenderRegular(
content: String,
tags: ImmutableListOfLists<String>,
callbackUri: String? = null,
authorPubKey: String? = null,
renderParagraph: @Composable (ParagraphState, state: RichTextViewerState, Dp, modifier: Modifier) -> Unit,
) {
val state by remember(content, tags) { mutableStateOf(CachedRichTextParser.parseText(content, tags, callbackUri, authorPubKey)) }
val spaceWidth = measureSpaceWidth(LocalTextStyle.current)
val currentTextStyle = LocalTextStyle.current
val textStyle =
remember(currentTextStyle) {
currentTextStyle.copy(
lineHeight = 1.3.em,
)
}
Column {
// FlowRow doesn't work well with paragraphs. So we need to split them
state.paragraphs.forEach { paragraph ->
CompositionLocalProvider(
LocalLayoutDirection provides
if (paragraph.isRTL) {
LayoutDirection.Rtl
} else {
LayoutDirection.Ltr
},
LocalTextStyle provides textStyle,
) {
renderParagraph(
paragraph,
state,
spaceWidth,
Modifier.align(if (paragraph.isRTL) Alignment.End else Alignment.Start),
)
}
}
}
}
@Composable
fun measureSpaceWidth(textStyle: TextStyle): Dp {
val fontFamilyResolver = LocalFontFamilyResolver.current
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
return remember(fontFamilyResolver, density, layoutDirection, textStyle) {
val widthPx =
TextMeasurer(fontFamilyResolver, density, layoutDirection, 1)
.measure(" ", textStyle)
.size
.width
with(density) { widthPx.toDp() }
}
}
@Composable
fun RenderCustomEmoji(
word: String,
state: RichTextViewerState,
) {
CreateTextWithEmoji(
text = word,
emojis = state.customEmoji,
)
}
@Composable
fun BechLink(
word: String,
canPreview: Boolean,
quotesLeft: Int,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) {
val loadedLink by produceCachedState(cache = accountViewModel.bechLinkCache, key = word)
val baseNote = loadedLink?.baseNote
// Preload the placeholder's author NIP-65 outbox so a missing quoted event can be fetched from there.
baseNote?.author?.let { author ->
UserFinderFilterAssemblerSubscription(author, accountViewModel)
}
if (canPreview && quotesLeft > 0 && baseNote != null) {
Row {
DisplayFullNote(
note = baseNote,
extraChars = loadedLink?.nip19?.additionalChars?.ifBlank { null },
quotesLeft = quotesLeft,
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
nav = nav,
)
}
} else if (loadedLink?.nip19 != null) {
ClickableRoute(word, loadedLink?.nip19!!, accountViewModel, nav)
} else {
val text =
remember(word) {
if (word.length > 16) {
word.replaceRange(8, word.length - 8, ":")
} else {
word
}
}
Text(text = text, maxLines = 1)
}
}
@Composable
fun DisplayFullNote(
note: Note,
extraChars: String?,
quotesLeft: Int,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) {
LocalInlineQuoteRenderer.current.Render(note, quotesLeft, backgroundColor, accountViewModel, nav)
extraChars?.let { Text(it) }
}
@Composable
fun HashTag(
segment: HashTagSegment,
nav: INav,
) {
val primary = MaterialTheme.colorScheme.primary
val background = MaterialTheme.colorScheme.onBackground
val hashtagIcon: HashtagIcon? = checkForHashtagWithIcon(segment.hashtag)
val annotatedTermsString =
remember(segment.segmentText) {
buildAnnotatedString {
withStyle(SpanStyle(color = primary)) {
pushStringAnnotation("routeToHashtag", "")
append("#${segment.hashtag}")
pop()
}
if (hashtagIcon != null) {
withStyle(SpanStyle(color = primary)) {
pushStringAnnotation("routeToHashtag", "")
appendInlineContent("inlineContent", "[icon]")
pop()
}
}
segment.extras?.let { withStyle(SpanStyle(color = background)) { append(it) } }
}
}
Text(
text = annotatedTermsString,
modifier =
remember {
Modifier.clickable {
nav.nav(Route.Hashtag(segment.hashtag.lowercase()))
}
},
inlineContent =
if (hashtagIcon != null) {
mapOf("inlineContent" to inlineIcon(hashtagIcon))
} else {
emptyMap()
},
)
}
@Composable
private fun inlineIcon(hashtagIcon: HashtagIcon) =
InlineTextContent(inlinePlaceholder) {
Icon(
imageVector = hashtagIcon.icon,
contentDescription = hashtagIcon.description,
tint = Color.Unspecified,
modifier = hashtagIcon.modifier,
)
}
@Composable
fun TagLink(
word: HashIndexUserSegment,
accountViewModel: AccountViewModel,
nav: INav,
) {
LoadUser(baseUserHex = word.hex) {
if (it == null) {
Text(text = word.segmentText)
} else {
Row {
DisplayUserFromTag(it, accountViewModel, nav)
word.extras?.let { it2 ->
Text(text = it2)
}
}
}
}
}
@Composable
fun LoadNote(
baseNoteHex: String,
content: @Composable (Note?) -> Unit,
) {
var note by
remember(baseNoteHex) { mutableStateOf(LocalCache.getNoteIfExists(baseNoteHex)) }
if (note == null) {
LaunchedEffect(key1 = baseNoteHex) {
note = LocalCache.checkGetOrCreateNote(baseNoteHex)
}
}
content(note)
}
@Composable
fun TagLink(
word: HashIndexEventSegment,
canPreview: Boolean,
quotesLeft: Int,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) {
LoadNote(baseNoteHex = word.hex) {
if (it == null) {
Text(text = remember { word.segmentText.toShortDisplay() })
} else {
Row {
DisplayNoteFromTag(
it,
word.extras,
canPreview,
quotesLeft,
accountViewModel,
backgroundColor,
nav,
)
}
}
}
}
@Preview
@Composable
fun DisplayNoteFromTagPreview() {
val dummyPost =
TextNoteEvent(
id = "0b6d941c46411a95edb1c93da7ad6ca26370497d8c7b7d621f5cb59f48841bad",
pubKey = "6dd3b72e325da7383b275eef1c66131ba4664326e162bc060527509b4e33ae43",
createdAt = 1753988264,
tags = emptyArray(),
content = "test",
sig = "ec39e60722a083cccbd2d82d2827e13f5499fa7cbcedac5b76011a844c077473adb629d50d01fab147835ac6c8a3d5ba9aaddd87d6723f0c3c864b9119fc4356",
)
LocalCache.justConsume(dummyPost, null, true)
val note = LocalCache.getOrCreateNote(dummyPost.id)
ThemeComparisonColumn(
toPreview = {
ClickableTextPrimary(
text = "@${note.idNote().toShortDisplay()}",
onClick = { },
)
},
)
}
@Composable
private fun DisplayNoteFromTag(
baseNote: Note,
addedChars: String?,
canPreview: Boolean,
quotesLeft: Int,
accountViewModel: AccountViewModel,
backgroundColor: MutableState<Color>,
nav: INav,
) {
if (canPreview && quotesLeft > 0) {
LocalInlineQuoteRenderer.current.Render(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
} else {
ClickableTextPrimary(
text = "@${baseNote.idNote().toShortDisplay()}",
onClick = { routeFor(baseNote, accountViewModel.account)?.let { nav.nav(it) } },
)
}
addedChars?.ifBlank { null }?.let { Text(text = it) }
}
@Composable
private fun DisplayUserFromTag(
baseUser: User,
accountViewModel: AccountViewModel,
nav: INav,
) {
val meta by observeUserInfo(baseUser, accountViewModel)
val nickname by observeUserDisplayNickname(baseUser, accountViewModel)
val petName = nickname?.petName
CrossfadeIfEnabled(targetState = meta, label = "DisplayUserFromTag") {
Row {
CreateClickableTextWithEmoji(
clickablePart = remember(meta, petName) { petName ?: it?.info?.bestName() ?: baseUser.pubkeyDisplayHex() },
maxLines = 1,
route = remember(baseUser) { routeFor(baseUser) },
nav = nav,
tags = if (petName != null) nickname?.tags else it?.tags,
)
}
}
}
@@ -0,0 +1,92 @@
/*
* Copyright (c) 2025 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.commons.ui.richtext
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
/**
* What a front end plugs into the shared rich-text viewer: the [RichTextSegmentRenderer] for its
* platform leaves (zoomable media, LaTeX, payments, link previews, Blossom links) and its markdown
* renderer. The shared viewer parses, lays out paragraphs and renders text, emoji, hashtags and
* mentions itself, and asks this for everything else.
*
* A front end installs its own through [LocalRichTextPlatform] at its composition root (Android
* does so in its theme). Without one, [Plain] renders every platform segment as text, which is
* what previews and a front end still wiring its leaves get.
*/
@Stable
interface RichTextPlatform {
/** The segment renderer for one viewer call; it may close over every argument. */
fun segmentRenderer(
accountViewModel: AccountViewModel,
nav: INav,
backgroundColor: MutableState<Color>,
callbackUri: String?,
canPreview: Boolean,
): RichTextSegmentRenderer
/** Renders [content] that the parser recognised as markdown. */
@Composable
fun Markdown(
content: String,
tags: ImmutableListOfLists<String>,
canPreview: Boolean,
quotesLeft: Int,
backgroundColor: MutableState<Color>,
callbackUri: String?,
accountViewModel: AccountViewModel,
nav: INav,
)
/** Text for every platform segment, and markdown shown as its source. */
object Plain : RichTextPlatform {
override fun segmentRenderer(
accountViewModel: AccountViewModel,
nav: INav,
backgroundColor: MutableState<Color>,
callbackUri: String?,
canPreview: Boolean,
): RichTextSegmentRenderer = PlainTextSegmentRenderer
@Composable
override fun Markdown(
content: String,
tags: ImmutableListOfLists<String>,
canPreview: Boolean,
quotesLeft: Int,
backgroundColor: MutableState<Color>,
callbackUri: String?,
accountViewModel: AccountViewModel,
nav: INav,
) = Text(content)
}
}
/** The front end's [RichTextPlatform]. Static: it is set once at the root and never changes. */
val LocalRichTextPlatform = staticCompositionLocalOf<RichTextPlatform> { RichTextPlatform.Plain }