refactor(napplets): render browse rows via shared NoteCompose

Replace the bespoke NappletCard with NoteCompose — the same path the main feed
uses for kind 15129/35129 events. This reuses the author header, the shared
StaticWebsiteCard (title/description/source/servers/capability list + Open button
wired to NappletLauncher), and the standard reaction bar (reply/boost/like/zap),
instead of a second hand-rolled card that could drift from the feed and was
missing NoteCompose's timestamp, hidden-user handling, zap-amount menus, etc.

The new top bar + follow-list filter (defaultNappletsFollowList / matchAuthor)
are genuinely new and kept as-is.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
This commit is contained in:
Claude
2026-06-22 16:47:05 +00:00
parent 7aa5f10a72
commit 4cd71b140c
@@ -20,56 +20,42 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets
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.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SuggestionChip
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.napplet.NappletLauncher
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.ReactionsRow
import com.vitorpamplona.amethyst.ui.note.UserPicture
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser
import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.datasource.NappletsFilterAssemblerSubscription
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip5dNapplets.NamedNappletEvent
import com.vitorpamplona.quartz.nip5dNapplets.NappletManifest
import com.vitorpamplona.quartz.nip5dNapplets.RootNappletEvent
/**
* Lists the napplet manifests currently in the local cache (NIP-5D kinds 15129/35129) and opens
* the selected one in the sandboxed [NappletLauncher] host. The top bar carries a follow-list filter
* (like the Pictures/Articles feeds) and each row is a rich card with the author, declared
* capabilities, and the usual reaction bar (reply/boost/like/zap).
* Lists the napplet manifests currently in the local cache (NIP-5D kinds 15129/35129). The top bar
* carries a follow-list filter (like the Pictures/Articles feeds) and each row is rendered through the
* shared [NoteCompose] — the same path the main feed uses for these events — so it gets the author
* header, the [com.vitorpamplona.amethyst.commons.ui.note.StaticWebsiteCard] (title, description,
* capability chips, and an Open button wired to the sandboxed [NappletLauncher]), and the standard
* reaction bar (reply/boost/like/zap) for free, without a second card implementation that could drift.
*/
@Composable
fun NappletsScreen(
@@ -104,23 +90,15 @@ fun NappletsScreen(
)
}
} else {
val context = LocalContext.current
LazyColumn(Modifier.fillMaxSize().padding(padding)) {
items(visible, key = { it.id }) { event ->
val manifest = event as? NappletManifest ?: return@items
NappletCard(
event = event,
manifest = manifest,
val note = remember(event.id) { Amethyst.instance.cache.getOrCreateNote(event) }
NoteCompose(
baseNote = note,
modifier = Modifier.fillMaxWidth(),
quotesLeft = 3,
accountViewModel = accountViewModel,
nav = nav,
onClick = {
NappletLauncher.launch(
context = context,
manifest = manifest,
authorPubKey = event.pubKey,
identifier = (event as? NamedNappletEvent)?.identifier() ?: "",
)
},
)
HorizontalDivider()
}
@@ -128,73 +106,3 @@ fun NappletsScreen(
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun NappletCard(
event: Event,
manifest: NappletManifest,
accountViewModel: AccountViewModel,
nav: INav,
onClick: () -> Unit,
) {
val note = remember(event.id) { Amethyst.instance.cache.getOrCreateNote(event) }
Column(
modifier =
Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
Row(verticalAlignment = Alignment.CenterVertically) {
UserPicture(userHex = event.pubKey, size = 48.dp, accountViewModel = accountViewModel, nav = nav)
Spacer(Modifier.width(12.dp))
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(
text = manifest.title()?.ifBlank { null } ?: stringResource(R.string.napplet_untitled),
style = MaterialTheme.typography.titleMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
LoadUser(baseUserHex = event.pubKey, accountViewModel) { user ->
if (user != null) {
UsernameDisplay(user, accountViewModel = accountViewModel)
}
}
}
}
manifest.description()?.takeIf { it.isNotBlank() }?.let {
Text(
text = it,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
)
}
val requires = manifest.requires()
if (requires.isNotEmpty()) {
FlowRow(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
requires.forEach { capability ->
SuggestionChip(
onClick = onClick,
label = { Text(capability.replaceFirstChar { it.uppercase() }) },
)
}
}
}
ReactionsRow(
baseNote = note,
showReactionDetail = true,
addPadding = false,
editState = null,
accountViewModel = accountViewModel,
nav = nav,
)
}
}