mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
refactor(commons): shared ClickableUrl/ClickableEmail; Desktop drops its duplicate
Desktop's ClickableLink and Amethyst's ClickableUrl were near-identical: the only real differences are mouse-first styling (Desktop underlines + shows a hand cursor) and the open mechanism. But LocalUriHandler.openUri opens the browser on Android AND Desktop (and the mail client for mailto:), so the "open a link" logic never needed to be platform-specific. Add ClickableUrl/ClickableEmail to commons/ui/components on LocalUriHandler, with an `underline` flag so each front end keeps its exact look. Desktop's rich-text renderer now reuses them (underline = true) for url/email/link-preview/withdraw and its bespoke ClickableLink is deleted. Amethyst keeps its own blossom-intent- aware ClickableUrl (blossom never applies to plain links, so this path is equivalent); Phone stays platform-specific (Android dials, Desktop has no dialer). Verified: :commons JVM and :desktopApp compile. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LUiGxXMbjVmgspa1X15o1V
This commit is contained in:
+93
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.pointer.PointerIcon
|
||||
import androidx.compose.ui.input.pointer.pointerHoverIcon
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
|
||||
/**
|
||||
* A primary-colored, single-line clickable URL that opens via the platform
|
||||
* [LocalUriHandler] — the browser on Android, `Desktop.browse` on JVM — so the
|
||||
* "open a link" behavior is identical on every front end without a seam.
|
||||
*
|
||||
* [underline] + the hover cursor are mouse-first affordances: Desktop passes
|
||||
* `true`; touch front ends leave it `false`. A scheme-less [url] is opened as
|
||||
* `https://`.
|
||||
*/
|
||||
@Composable
|
||||
fun ClickableUrl(
|
||||
url: String,
|
||||
displayText: String = url,
|
||||
modifier: Modifier = Modifier,
|
||||
underline: Boolean = false,
|
||||
style: TextStyle = LocalTextStyle.current,
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
Text(
|
||||
text = displayText,
|
||||
style = style,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
textDecoration = if (underline) TextDecoration.Underline else null,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.MiddleEllipsis,
|
||||
modifier =
|
||||
modifier.pointerHoverIcon(PointerIcon.Hand).clickable {
|
||||
runCatching {
|
||||
uriHandler.openUri(if (url.contains("://")) url else "https://$url")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A primary-colored clickable email that opens the platform mail client via
|
||||
* [LocalUriHandler] (`mailto:`). Strips a leading `mailto:` from the display text.
|
||||
* [underline] + hover cursor are the Desktop mouse-first affordances.
|
||||
*/
|
||||
@Composable
|
||||
fun ClickableEmail(
|
||||
address: String,
|
||||
modifier: Modifier = Modifier,
|
||||
underline: Boolean = false,
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val display = remember(address) { address.removePrefix("mailto:") }
|
||||
Text(
|
||||
text = display,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
textDecoration = if (underline) TextDecoration.Underline else null,
|
||||
modifier =
|
||||
modifier.pointerHoverIcon(PointerIcon.Hand).clickable {
|
||||
runCatching { uriHandler.openUri("mailto:$display") }
|
||||
},
|
||||
)
|
||||
}
|
||||
+7
-45
@@ -70,6 +70,8 @@ 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.ClickableEmail
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.ClickableUrl
|
||||
import com.vitorpamplona.amethyst.commons.ui.markdown.RenderMarkdown
|
||||
import com.vitorpamplona.amethyst.commons.ui.richtext.LocalRichTextInteractions
|
||||
import com.vitorpamplona.amethyst.commons.ui.richtext.LocalRichTextSegmentRenderer
|
||||
@@ -204,7 +206,7 @@ class DesktopRichTextSegmentRenderer(
|
||||
contentScale = ContentScale.Fit,
|
||||
)
|
||||
is PdfSegment -> RenderPdfCard(segment.segmentText)
|
||||
else -> ClickableLink(segment.segmentText, segment.segmentText)
|
||||
else -> ClickableUrl(segment.segmentText, underline = true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,7 +295,7 @@ class DesktopRichTextSegmentRenderer(
|
||||
when (segment) {
|
||||
is InvoiceSegment -> RenderInvoiceCard(segment.segmentText, callbacks)
|
||||
is CashuSegment -> RenderCashuCard(segment.segmentText)
|
||||
is WithdrawSegment -> ClickableLink(segment.segmentText, segment.segmentText)
|
||||
is WithdrawSegment -> ClickableUrl(segment.segmentText, underline = true)
|
||||
else -> Text(segment.segmentText, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
}
|
||||
@@ -302,35 +304,20 @@ class DesktopRichTextSegmentRenderer(
|
||||
override fun LinkPreview(
|
||||
url: String,
|
||||
modifier: Modifier,
|
||||
) = ClickableLink(url, url)
|
||||
) = ClickableUrl(url, underline = true)
|
||||
|
||||
@Composable
|
||||
override fun Url(
|
||||
url: String,
|
||||
displayText: String,
|
||||
modifier: Modifier,
|
||||
) = ClickableLink(url, displayText)
|
||||
) = ClickableUrl(url, displayText, underline = true)
|
||||
|
||||
@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"))
|
||||
}
|
||||
},
|
||||
)
|
||||
) = ClickableEmail(address, underline = true)
|
||||
|
||||
@Composable
|
||||
override fun Phone(
|
||||
@@ -376,31 +363,6 @@ class DesktopRichTextSegmentRenderer(
|
||||
) = RenderSecretEmoji(segment.segmentText)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ClickableLink(
|
||||
url: String,
|
||||
displayText: String,
|
||||
) {
|
||||
Text(
|
||||
text = displayText,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
textDecoration = TextDecoration.Underline,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier =
|
||||
Modifier
|
||||
.pointerHoverIcon(PointerIcon.Hand)
|
||||
.clickable {
|
||||
runCatching {
|
||||
java.awt.Desktop
|
||||
.getDesktop()
|
||||
.browse(URI(url))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderBechSegment(
|
||||
segment: BechSegment,
|
||||
|
||||
Reference in New Issue
Block a user