refactor: redesign NIP-5A rendering as website preview card

Replace the file-listing style with a UrlPreviewCard-like layout
showing nsite host URL, title, description, and optional favicon.
Remove path rendering and unused string resources.

https://claude.ai/code/session_01XTmqQ9QatUA7aPCWt7NHNt
This commit is contained in:
Claude
2026-03-30 00:25:09 +00:00
parent 776bc6d36f
commit 07f16c2efe
2 changed files with 89 additions and 140 deletions
@@ -20,38 +20,29 @@
*/ */
package com.vitorpamplona.amethyst.ui.note.types package com.vitorpamplona.amethyst.ui.note.types
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column 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.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.runtime.remember
import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.ui.theme.MaxWidthWithHorzPadding
import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.innerPostModifier
import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.previewCardImageModifier
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.amethyst.ui.theme.subtleBorder import com.vitorpamplona.quartz.nip19Bech32.toNpub
import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent
import com.vitorpamplona.quartz.nip5aStaticWebsites.RootSiteEvent import com.vitorpamplona.quartz.nip5aStaticWebsites.RootSiteEvent
import com.vitorpamplona.quartz.nip5aStaticWebsites.tags.PathTag
@Composable @Composable
fun RenderRootSiteEvent( fun RenderRootSiteEvent(
@@ -61,13 +52,18 @@ fun RenderRootSiteEvent(
) { ) {
val event = baseNote.event as? RootSiteEvent ?: return val event = baseNote.event as? RootSiteEvent ?: return
RenderStaticWebsite( val npub =
remember(event.pubKey) {
event.pubKey.hexToByteArray().toNpub()
}
RenderStaticWebsitePreview(
title = event.title(), title = event.title(),
description = event.description(), description = event.description(),
source = event.source(), npub = npub,
servers = event.servers(),
paths = event.paths(),
identifier = null, identifier = null,
faviconHash = event.paths().firstOrNull { it.path == "/favicon.ico" }?.hash,
servers = event.servers(),
) )
} }
@@ -79,129 +75,87 @@ fun RenderNamedSiteEvent(
) { ) {
val event = baseNote.event as? NamedSiteEvent ?: return val event = baseNote.event as? NamedSiteEvent ?: return
RenderStaticWebsite( val npub =
remember(event.pubKey) {
event.pubKey.hexToByteArray().toNpub()
}
RenderStaticWebsitePreview(
title = event.title(), title = event.title(),
description = event.description(), description = event.description(),
source = event.source(), npub = npub,
servers = event.servers(),
paths = event.paths(),
identifier = event.identifier(), identifier = event.identifier(),
faviconHash = event.paths().firstOrNull { it.path == "/favicon.ico" }?.hash,
servers = event.servers(),
) )
} }
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
private fun RenderStaticWebsite( private fun RenderStaticWebsitePreview(
title: String?, title: String?,
description: String?, description: String?,
source: String?, npub: String,
servers: List<String>,
paths: List<PathTag>,
identifier: String?, identifier: String?,
faviconHash: String?,
servers: List<String>,
) { ) {
Row( val nsiteHost =
modifier = if (identifier != null) {
Modifier "$npub.$identifier.nsite.lol"
.clip(shape = QuoteBorder) } else {
.border( "$npub.nsite.lol"
1.dp,
MaterialTheme.colorScheme.subtleBorder,
QuoteBorder,
).padding(Size10dp),
) {
Column {
val displayTitle =
title
?: identifier
?: stringRes(id = R.string.nsite_root_site)
Text(
text = stringRes(id = R.string.nsite_title, displayTitle),
style = MaterialTheme.typography.titleMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth(),
)
description?.let {
Text(
text = it,
modifier = Modifier.fillMaxWidth().padding(vertical = Size5dp),
maxLines = 3,
overflow = TextOverflow.Ellipsis,
)
}
HorizontalDivider(thickness = DividerThickness)
source?.let {
Row(Modifier.fillMaxWidth().padding(top = Size5dp)) {
Text(
text = stringRes(id = R.string.nsite_source),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Spacer(modifier = StdHorzSpacer)
ClickableUrl(
url = it,
urlText = it.removePrefix("https://").removePrefix("http://"),
)
}
}
if (servers.isNotEmpty()) {
Row(Modifier.fillMaxWidth().padding(top = Size5dp)) {
Text(
text = stringRes(id = R.string.nsite_servers),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Spacer(modifier = StdHorzSpacer)
Column {
servers.forEach { server ->
ClickableUrl(
url = server,
urlText = server.removePrefix("https://").removePrefix("http://"),
)
}
}
}
}
if (paths.isNotEmpty()) {
Row(Modifier.fillMaxWidth().padding(top = Size5dp)) {
Text(
text = stringRes(id = R.string.nsite_files, paths.size),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
FlowRow(
modifier = Modifier.fillMaxWidth().padding(top = Size5dp),
horizontalArrangement = Arrangement.spacedBy(Size5dp),
verticalArrangement = Arrangement.spacedBy(Size5dp),
) {
paths.take(10).forEach { path ->
Text(
text = path.path,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
if (paths.size > 10) {
Text(
text = stringRes(id = R.string.nsite_more_files, paths.size - 10),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
)
}
}
}
} }
val nsiteUrl = "https://$nsiteHost"
val faviconUrl =
if (faviconHash != null && servers.isNotEmpty()) {
"${servers.first()}/$faviconHash"
} else {
null
}
val uri = LocalUriHandler.current
Column(
modifier = MaterialTheme.colorScheme.innerPostModifier,
) {
faviconUrl?.let {
AsyncImage(
model = it,
contentDescription = title,
modifier = previewCardImageModifier,
)
}
Text(
text = nsiteHost,
style = MaterialTheme.typography.bodySmall,
modifier = MaxWidthWithHorzPadding,
color = Color.Gray,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
text = title ?: identifier ?: stringRes(id = R.string.nsite_root_site),
style = MaterialTheme.typography.bodyMedium,
modifier = MaxWidthWithHorzPadding,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
description?.let {
Text(
text = it,
style = MaterialTheme.typography.bodySmall,
modifier = MaxWidthWithHorzPadding,
color = Color.Gray,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
)
}
Spacer(modifier = DoubleVertSpacer)
} }
} }
-5
View File
@@ -1488,12 +1488,7 @@
<string name="git_repository">Git Repository: %1$s</string> <string name="git_repository">Git Repository: %1$s</string>
<string name="git_web_address">Web:</string> <string name="git_web_address">Web:</string>
<string name="git_clone_address">Clone:</string> <string name="git_clone_address">Clone:</string>
<string name="nsite_title">Static Website: %1$s</string>
<string name="nsite_root_site">Root Site</string> <string name="nsite_root_site">Root Site</string>
<string name="nsite_source">Source:</string>
<string name="nsite_servers">Servers:</string>
<string name="nsite_files">%1$d files</string>
<string name="nsite_more_files">+%1$d more</string>
<string name="existed_since">OTS: %1$s</string> <string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">Timestamp Proof</string> <string name="ots_info_title">Timestamp Proof</string>