fix(dvm-card): star top-right + move reactions to bottom row

Putting the star back where the user actually sees DVMs first — the
Discover Content card. Two changes that together stop the layout
from breaking:

- FavoriteDvmToggle now uses ClickableBox + small Icon (no IconButton
  padding) so it occupies the same compact footprint as LikeReaction.
  Adding it to LeftPictureLayout's title row no longer inflates the
  row height.
- DVMCard's title row now holds only the name (weight 1) plus the
  star toggle on the right. LikeReaction and ZapReaction move down
  to the bottom row, pushed to the far right with a Spacer(weight 1)
  so the amount/personalised chips stay on the left.
This commit is contained in:
Claude
2026-04-18 23:02:53 +00:00
parent 8555074c3f
commit 309e474a3d
2 changed files with 61 additions and 61 deletions
@@ -21,11 +21,9 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -40,8 +38,8 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
@@ -51,12 +49,12 @@ import com.vitorpamplona.amethyst.ui.note.LikeReaction
import com.vitorpamplona.amethyst.ui.note.ZapReaction import com.vitorpamplona.amethyst.ui.note.ZapReaction
import com.vitorpamplona.amethyst.ui.note.elements.BannerImage import com.vitorpamplona.amethyst.ui.note.elements.BannerImage
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.FavoriteDvmToggle
import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.observeAppDefinition import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.observeAppDefinition
import com.vitorpamplona.amethyst.ui.theme.HalfTopPadding import com.vitorpamplona.amethyst.ui.theme.HalfTopPadding
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing5dp import com.vitorpamplona.amethyst.ui.theme.RowColSpacing5dp
import com.vitorpamplona.amethyst.ui.theme.SimpleImageBorder import com.vitorpamplona.amethyst.ui.theme.SimpleImageBorder
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.bitcoinColor import com.vitorpamplona.amethyst.ui.theme.bitcoinColor
import com.vitorpamplona.amethyst.ui.theme.grayText import com.vitorpamplona.amethyst.ui.theme.grayText
import com.vitorpamplona.amethyst.ui.theme.nip05 import com.vitorpamplona.amethyst.ui.theme.nip05
@@ -118,25 +116,12 @@ fun RenderContentDVMThumb(
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) )
Spacer(modifier = StdVertSpacer) if (baseNote is AddressableNote) {
Row( FavoriteDvmToggle(
verticalAlignment = CenterVertically, appDefinitionNote = baseNote,
horizontalArrangement = RowColSpacing5dp,
) {
LikeReaction(
baseNote = baseNote,
grayTint = MaterialTheme.colorScheme.onSurface,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav,
) )
} }
Spacer(modifier = StdHorzSpacer)
ZapReaction(
baseNote = baseNote,
grayTint = MaterialTheme.colorScheme.onSurface,
accountViewModel = accountViewModel,
nav = nav,
)
}, },
onDescription = { onDescription = {
card.description?.let { card.description?.let {
@@ -168,22 +153,16 @@ fun RenderContentDVMThumb(
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
amount = card.amount + " Sats" amount = card.amount + " Sats"
} }
Row( Text(
verticalAlignment = CenterVertically, textAlign = TextAlign.End,
horizontalArrangement = Arrangement.Absolute.Right, text = " $amount ",
) { color = color,
Text( maxLines = 1,
textAlign = TextAlign.End, modifier =
text = " $amount ", Modifier
color = color, .border(Dp(.1f), color, shape = RoundedCornerShape(20)),
maxLines = 3, fontSize = 12.sp,
modifier = )
Modifier
.weight(1f, fill = false)
.border(Dp(.1f), color, shape = RoundedCornerShape(20)),
fontSize = 12.sp,
)
}
} }
Spacer(modifier = StdHorzSpacer) Spacer(modifier = StdHorzSpacer)
card.personalized?.let { card.personalized?.let {
@@ -196,24 +175,35 @@ fun RenderContentDVMThumb(
color = MaterialTheme.colorScheme.nip05 color = MaterialTheme.colorScheme.nip05
name = "Generic" name = "Generic"
} }
Spacer(modifier = StdVertSpacer) Text(
Row( textAlign = TextAlign.End,
verticalAlignment = CenterVertically, text = " $name ",
horizontalArrangement = Arrangement.Absolute.Right, color = color,
) { maxLines = 1,
Text( modifier =
textAlign = TextAlign.End, Modifier
text = " $name ", .border(Dp(.1f), color, shape = RoundedCornerShape(20)),
color = color, fontSize = 12.sp,
maxLines = 3, )
modifier = }
Modifier Spacer(modifier = Modifier.weight(1f))
.padding(start = 4.dp) Row(
.weight(1f, fill = false) verticalAlignment = CenterVertically,
.border(Dp(.1f), color, shape = RoundedCornerShape(20)), horizontalArrangement = RowColSpacing5dp,
fontSize = 12.sp, ) {
) LikeReaction(
} baseNote = baseNote,
grayTint = MaterialTheme.colorScheme.onSurface,
accountViewModel = accountViewModel,
nav,
)
Spacer(modifier = StdHorzSpacer)
ZapReaction(
baseNote = baseNote,
grayTint = MaterialTheme.colorScheme.onSurface,
accountViewModel = accountViewModel,
nav = nav,
)
} }
}, },
) )
@@ -24,7 +24,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Star import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.outlined.StarBorder import androidx.compose.material.icons.outlined.StarBorder
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -33,6 +32,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap
import com.vitorpamplona.amethyst.ui.components.ClickableBox
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.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
@@ -40,14 +40,24 @@ import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.NIP90ContentDiscoveryRequestEvent import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.NIP90ContentDiscoveryRequestEvent
/**
* Inline star toggle that follows / unfollows a NIP-90 content-discovery DVM.
*
* Uses [ClickableBox] (no [androidx.compose.material3.IconButton] padding) so it
* fits in a `LeftPictureLayout` title row alongside other compact reactions
* without inflating the row to 48dp.
*
* Hidden until the underlying [AppDefinitionEvent] loads and we can confirm the
* DVM advertises kind 5300 — favouriting any other DVM type would only stall on
* a 6300 reply that never comes.
*/
@Composable @Composable
fun FavoriteDvmToggle( fun FavoriteDvmToggle(
appDefinitionNote: AddressableNote, appDefinitionNote: AddressableNote,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
iconSizeModifier: Modifier = Size20Modifier,
) { ) {
// Only NIP-90 content-discovery DVMs (kind 5300) produce a feed; hide the toggle
// for any other DVM type so users don't favourite something that would never reply.
val supportsContentDiscovery by val supportsContentDiscovery by
observeNoteAndMap(appDefinitionNote, accountViewModel) { note -> observeNoteAndMap(appDefinitionNote, accountViewModel) { note ->
(note.event as? AppDefinitionEvent)?.includeKind(NIP90ContentDiscoveryRequestEvent.KIND) == true (note.event as? AppDefinitionEvent)?.includeKind(NIP90ContentDiscoveryRequestEvent.KIND) == true
@@ -60,7 +70,8 @@ fun FavoriteDvmToggle(
val isFavorite = favorites.contains(appDefinitionNote.address) val isFavorite = favorites.contains(appDefinitionNote.address)
IconButton( ClickableBox(
modifier = modifier,
onClick = { onClick = {
if (isFavorite) { if (isFavorite) {
accountViewModel.unfollowFavoriteDvm(appDefinitionNote.address) accountViewModel.unfollowFavoriteDvm(appDefinitionNote.address)
@@ -73,20 +84,19 @@ fun FavoriteDvmToggle(
) )
} }
}, },
modifier = modifier,
) { ) {
if (isFavorite) { if (isFavorite) {
Icon( Icon(
imageVector = Icons.Filled.Star, imageVector = Icons.Filled.Star,
contentDescription = stringRes(R.string.remove_dvm_from_favorites), contentDescription = stringRes(R.string.remove_dvm_from_favorites),
modifier = Size20Modifier, modifier = iconSizeModifier,
tint = MaterialTheme.colorScheme.primary, tint = MaterialTheme.colorScheme.primary,
) )
} else { } else {
Icon( Icon(
imageVector = Icons.Outlined.StarBorder, imageVector = Icons.Outlined.StarBorder,
contentDescription = stringRes(R.string.add_dvm_to_favorites), contentDescription = stringRes(R.string.add_dvm_to_favorites),
modifier = Size20Modifier, modifier = iconSizeModifier,
tint = MaterialTheme.colorScheme.onSurface, tint = MaterialTheme.colorScheme.onSurface,
) )
} }