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
import androidx.compose.foundation.border
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.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.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalUriHandler
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.model.Note
import com.vitorpamplona.amethyst.ui.components.ClickableUrl
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size5dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.MaxWidthWithHorzPadding
import com.vitorpamplona.amethyst.ui.theme.innerPostModifier
import com.vitorpamplona.amethyst.ui.theme.previewCardImageModifier
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip19Bech32.toNpub
import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent
import com.vitorpamplona.quartz.nip5aStaticWebsites.RootSiteEvent
import com.vitorpamplona.quartz.nip5aStaticWebsites.tags.PathTag
@Composable
fun RenderRootSiteEvent(
@@ -61,13 +52,18 @@ fun RenderRootSiteEvent(
) {
val event = baseNote.event as? RootSiteEvent ?: return
RenderStaticWebsite(
val npub =
remember(event.pubKey) {
event.pubKey.hexToByteArray().toNpub()
}
RenderStaticWebsitePreview(
title = event.title(),
description = event.description(),
source = event.source(),
servers = event.servers(),
paths = event.paths(),
npub = npub,
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
RenderStaticWebsite(
val npub =
remember(event.pubKey) {
event.pubKey.hexToByteArray().toNpub()
}
RenderStaticWebsitePreview(
title = event.title(),
description = event.description(),
source = event.source(),
servers = event.servers(),
paths = event.paths(),
npub = npub,
identifier = event.identifier(),
faviconHash = event.paths().firstOrNull { it.path == "/favicon.ico" }?.hash,
servers = event.servers(),
)
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun RenderStaticWebsite(
private fun RenderStaticWebsitePreview(
title: String?,
description: String?,
source: String?,
servers: List<String>,
paths: List<PathTag>,
npub: String,
identifier: String?,
faviconHash: String?,
servers: List<String>,
) {
Row(
modifier =
Modifier
.clip(shape = QuoteBorder)
.border(
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 nsiteHost =
if (identifier != null) {
"$npub.$identifier.nsite.lol"
} else {
"$npub.nsite.lol"
}
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_web_address">Web:</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_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="ots_info_title">Timestamp Proof</string>