mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-11 08:47:33 +00:00
feat(nutzap): dedicated cashu row in ReactionDetailGallery
Phase 4 of the nutzap UX integration. The expanded reaction view on a note now shows a cashu row between the lightning row and the onchain row, matching the rail visual hierarchy used elsewhere (zap chip popup → notifications card → reaction-detail gallery). New `NutzapGallery.kt` modelled on `OnchainZapGallery.kt`: - `WatchNutzapsAndRenderGallery` subscribes to the same per-note zap flow the lightning + onchain galleries already use. Memoizes on the nutzaps map reference (immutable per mutation) so a lightning zap arrival on the same note doesn't recompose this row, and renders only when at least one entry exists. - Cashu icon (CustomHashTagIcons.Cashu, tint=Unspecified) followed by a FlowRow of sender avatars with the claimed sat amount overlay. Unlike onchain, there's no UNVERIFIED/PENDING gating — every entry shows at full opacity because nutzap verification is the recipient wallet's job at redeem time (the reaction-detail gallery is just showing "who sent what to this note"). - Tapping an entry navigates to the sender's profile, same as the other galleries. Wired into `ReactionsRow.ReactionDetailGallery` between `WatchZapAndRenderGallery` and `WatchOnchainZapsAndRenderGallery`, so the three zap rails read top-to-bottom in the order they tend to surface for a given recipient (LN cheapest/easiest, cashu mint-mediated, onchain final-settlement). That closes the 4-phase nutzap UX work: - Phase 0: success toast on send (sendNutzap previously silent) - Phase 1: Note.nutzaps + LocalCache wiring; reaction-row counter and icon highlight come along for free via zapsAmount and isZappedBy extension. - Phase 3: notifications — NotificationFeedFilter kind add, NutzapUserSetCard for per-sender aggregates, cashu rail in MultiSetCard, NutzapUserSetCompose card renderer. - Phase 4 (this): dedicated cashu row in the expanded reaction view.
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* 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.ui.note
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
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.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
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.graphics.Color
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.hashtags.Cashu
|
||||
import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons
|
||||
import com.vitorpamplona.amethyst.commons.model.NutzapEntry
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteZaps
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdStartPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.WidthAuthorPictureModifier
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.BigDecimal
|
||||
|
||||
private fun onNutzapEntryClick(
|
||||
entry: NutzapEntry,
|
||||
nav: INav,
|
||||
) {
|
||||
entry.source.author?.let { nav.nav(routeFor(it)) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun WatchNutzapsAndRenderGallery(
|
||||
baseNote: Note,
|
||||
nav: INav,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
// Same flow the lightning + onchain galleries subscribe to. The flow
|
||||
// fires on every kind of zap mutation on this note, so memoize on the
|
||||
// nutzap map reference (immutable; allocated fresh per mutation by
|
||||
// Note.addNutzap) to keep recomposition off the hot lightning path.
|
||||
val zapsState by observeNoteZaps(baseNote, accountViewModel)
|
||||
val nutzapsMap = zapsState?.note?.nutzaps
|
||||
val entries =
|
||||
remember(nutzapsMap) {
|
||||
nutzapsMap?.values?.toImmutableList() ?: persistentListOf()
|
||||
}
|
||||
|
||||
if (entries.isNotEmpty()) {
|
||||
RenderNutzapGalleryRow(entries, nav, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderNutzapGalleryRow(
|
||||
entries: ImmutableList<NutzapEntry>,
|
||||
nav: INav,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Box(modifier = WidthAuthorPictureModifier) {
|
||||
// tint=Unspecified preserves the multi-tone cashu glyph,
|
||||
// same convention as the notifications and zap-picker
|
||||
// surfaces. Keeping the rail visually distinct from the
|
||||
// lightning bolt above it is the whole point of this row.
|
||||
Icon(
|
||||
imageVector = CustomHashTagIcons.Cashu,
|
||||
contentDescription = stringRes(R.string.nutzap),
|
||||
modifier = Modifier.size(Size25dp).align(Alignment.TopEnd),
|
||||
tint = Color.Unspecified,
|
||||
)
|
||||
}
|
||||
|
||||
NutzapAuthorGallery(entries, nav, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun NutzapAuthorGallery(
|
||||
entries: ImmutableList<NutzapEntry>,
|
||||
nav: INav,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
Column(modifier = StdStartPadding) {
|
||||
FlowRow {
|
||||
entries.forEach { entry ->
|
||||
NutzapEntryAvatar(entry, nav, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NutzapEntryAvatar(
|
||||
entry: NutzapEntry,
|
||||
nav: INav,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val user = entry.source.author
|
||||
val amountText =
|
||||
remember(entry.claimedSats) {
|
||||
if (entry.claimedSats > 0L) showAmount(BigDecimal.valueOf(entry.claimedSats)) else ""
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Size35Modifier.clickable { onNutzapEntryClick(entry, nav) },
|
||||
contentAlignment = Alignment.BottomCenter,
|
||||
) {
|
||||
WatchUserMetadataAndFollowsAndRenderUserProfilePictureOrDefaultAuthor(
|
||||
user,
|
||||
accountViewModel,
|
||||
)
|
||||
|
||||
if (amountText.isNotEmpty()) {
|
||||
CrossfadeToDisplayAmount(amountText)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -557,6 +557,7 @@ private fun ReactionDetailGallery(
|
||||
) {
|
||||
Column {
|
||||
WatchZapAndRenderGallery(baseNote, backgroundColor, nav, accountViewModel)
|
||||
WatchNutzapsAndRenderGallery(baseNote, nav, accountViewModel)
|
||||
WatchOnchainZapsAndRenderGallery(baseNote, nav, accountViewModel)
|
||||
WatchBoostsAndRenderGallery(baseNote, nav, accountViewModel)
|
||||
WatchReactionsAndRenderGallery(baseNote, nav, accountViewModel)
|
||||
|
||||
Reference in New Issue
Block a user