diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/AmethystRichText.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/AmethystRichText.kt index 0bab9aad6d..81c42aa64e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/AmethystRichText.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/AmethystRichText.kt @@ -32,7 +32,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalUriHandler import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists import com.vitorpamplona.amethyst.commons.richtext.BlossomUriSegment import com.vitorpamplona.amethyst.commons.richtext.CachedRichTextParser @@ -178,6 +177,25 @@ class AmethystRichTextSegmentRenderer( modifier: Modifier, ) = LoadUrlPreview(url, url, callbackUri, accountViewModel, nav) + @Composable + override fun Url( + url: String, + displayText: String, + modifier: Modifier, + ) = ClickableUrl(displayText, url) + + @Composable + override fun Email( + address: String, + modifier: Modifier, + ) = ClickableEmail(address) + + @Composable + override fun Phone( + number: String, + modifier: Modifier, + ) = ClickablePhone(number) + @Composable override fun RelayLink( segment: Segment, @@ -266,13 +284,9 @@ fun CommonsBackedRichTextViewer( AmethystRichTextSegmentRenderer(accountViewModel, nav, backgroundColor, callbackUri, canPreview) } - val uriHandler = LocalUriHandler.current val interactions = - remember(nav, uriHandler) { + remember(nav) { RichTextInteractions( - onOpenUrl = { runCatching { uriHandler.openUri(it) } }, - onOpenEmail = { runCatching { uriHandler.openUri("mailto:$it") } }, - onOpenPhone = { runCatching { uriHandler.openUri("tel:$it") } }, onClickHashtag = { nav.nav(Route.Hashtag(it.lowercase())) }, ) } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/richtext/RichTextSegmentRenderer.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/richtext/RichTextSegmentRenderer.kt index b26525e557..ac91226e82 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/richtext/RichTextSegmentRenderer.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/richtext/RichTextSegmentRenderer.kt @@ -141,6 +141,32 @@ interface RichTextSegmentRenderer { modifier: Modifier, ) + /** + * A plain external link rendered inline (no preview): the no-preview [LinkPreview] + * fallback and schemeless URLs. [displayText] is what the user sees; [url] is where + * it opens. Each front end styles the link and owns how it opens. + */ + @Composable + fun Url( + url: String, + displayText: String, + modifier: Modifier, + ) + + /** An email address. Each front end styles it and owns the compose/open action. */ + @Composable + fun Email( + address: String, + modifier: Modifier, + ) + + /** A phone number. Each front end styles it and owns the dial action. */ + @Composable + fun Phone( + number: String, + modifier: Modifier, + ) + /** A relay URL, NIP-29 group invite, or Concord invite chip. */ @Composable fun RelayLink( @@ -168,18 +194,14 @@ interface RichTextSegmentRenderer { } /** - * Presentation-agnostic activations for the segments the shared core renders - * itself. The *action* is unambiguous on every platform (open a URL, dial a - * number, jump to a hashtag); only how the trigger looks/feels differs, which is - * a Modifier concern the core applies. Anything whose action itself diverges by - * platform (a mention that navigates vs. pops a hover-card) belongs in + * Activations for the segments the shared core renders itself. Only hashtags + * qualify: the core draws the chip (with the shared inline icon) identically on + * every platform, and only the navigation target differs — a pure callback. Links, + * mail, phone, mentions, etc. render *and* act per-platform, so they live on * [RichTextSegmentRenderer], not here. */ @Immutable data class RichTextInteractions( - val onOpenUrl: (url: String) -> Unit = {}, - val onOpenEmail: (address: String) -> Unit = {}, - val onOpenPhone: (number: String) -> Unit = {}, val onClickHashtag: (hashtag: String) -> Unit = {}, ) @@ -244,6 +266,25 @@ object PlainTextSegmentRenderer : RichTextSegmentRenderer { modifier: Modifier, ) = Text(url, modifier) + @Composable + override fun Url( + url: String, + displayText: String, + modifier: Modifier, + ) = Text(displayText, modifier) + + @Composable + override fun Email( + address: String, + modifier: Modifier, + ) = Text(address, modifier) + + @Composable + override fun Phone( + number: String, + modifier: Modifier, + ) = Text(number, modifier) + @Composable override fun RelayLink( segment: Segment, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/richtext/RichTextViewer.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/richtext/RichTextViewer.kt index 78c28fbd6d..d89976b9b3 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/richtext/RichTextViewer.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/richtext/RichTextViewer.kt @@ -156,8 +156,11 @@ private fun RenderWord( is RegularTextSegment -> Text(word.segmentText) is EmojiSegment -> RenderCustomEmoji(word.segmentText, state.customEmoji) is HashTagSegment -> HashTagText(word) { actions.onClickHashtag(word.hashtag) } - is EmailSegment -> ClickableSpan(word.segmentText) { actions.onOpenEmail(word.segmentText) } - is PhoneSegment -> ClickableSpan(word.segmentText) { actions.onOpenPhone(word.segmentText) } + + // Presentation + CTA are platform-owned: each front end styles its own + // clickable link/mail/phone and decides how to open it. + is EmailSegment -> renderer.Email(word.segmentText, Modifier) + is PhoneSegment -> renderer.Phone(word.segmentText, Modifier) // Divergent media — presentation and CTA are platform-owned. is ImageSegment, is VideoSegment, is PdfSegment, is Base64Segment, is BlossomUriSegment -> @@ -169,11 +172,11 @@ private fun RenderWord( if (canPreview) { renderer.LinkPreview(word.segmentText, Modifier) } else { - ClickableSpan(word.segmentText) { actions.onOpenUrl(word.segmentText) } + renderer.Url(word.segmentText, word.segmentText, Modifier) } is SchemelessUrlSegment -> - ClickableSpan(word.segmentText) { actions.onOpenUrl("https://${word.segmentText}") } + renderer.Url("https://${word.segmentText}", word.segmentText, Modifier) is NowhereLinkSegment -> renderer.NowhereLink(word, canPreview, Modifier) is RelayUrlSegment, is RelayGroupLinkSegment, is ConcordInviteLinkSegment -> @@ -192,18 +195,6 @@ private fun RenderWord( } } -@Composable -private fun ClickableSpan( - text: String, - onClick: () -> Unit, -) { - Text( - text = text, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.clickable(onClick = onClick), - ) -} - private val HashtagIconPlaceholder = Placeholder(width = 17.sp, height = 17.sp, placeholderVerticalAlign = PlaceholderVerticalAlign.Center) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/DesktopRichText.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/DesktopRichText.kt index 6804aeea43..9f70abdca0 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/DesktopRichText.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/DesktopRichText.kt @@ -125,21 +125,6 @@ fun DesktopRichText( val interactions = remember(callbacks) { RichTextInteractions( - onOpenUrl = { - runCatching { - java.awt.Desktop - .getDesktop() - .browse(URI(it)) - } - }, - onOpenEmail = { - runCatching { - java.awt.Desktop - .getDesktop() - .browse(URI("mailto:$it")) - } - }, - onOpenPhone = { }, onClickHashtag = { callbacks.onHashtagClick?.invoke(it) }, ) } @@ -319,6 +304,44 @@ class DesktopRichTextSegmentRenderer( modifier: Modifier, ) = ClickableLink(url, url) + @Composable + override fun Url( + url: String, + displayText: String, + modifier: Modifier, + ) = ClickableLink(url, displayText) + + @Composable + override fun Email( + address: String, + modifier: Modifier, + ) = Text( + text = address, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.primary, + textDecoration = TextDecoration.Underline, + modifier = + Modifier + .pointerHoverIcon(PointerIcon.Hand) + .clickable { + runCatching { + java.awt.Desktop + .getDesktop() + .browse(URI("mailto:$address")) + } + }, + ) + + @Composable + override fun Phone( + number: String, + modifier: Modifier, + ) = Text( + text = number, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + ) + @Composable override fun RelayLink( segment: Segment,