diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 650c6c0ad7..1b3ff9df30 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -90,7 +90,6 @@ import com.vitorpamplona.amethyst.ui.note.elements.Reward import com.vitorpamplona.amethyst.ui.note.elements.ShowForkInformation import com.vitorpamplona.amethyst.ui.note.elements.StaleRelayHint import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo -import com.vitorpamplona.amethyst.ui.note.types.ActivityActionChip import com.vitorpamplona.amethyst.ui.note.types.BadgeDisplay import com.vitorpamplona.amethyst.ui.note.types.DisplayBlockedRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayBroadcastRelayList @@ -1737,22 +1736,18 @@ fun FirstUserInfoRow( null } - Row(Modifier.weight(1f), verticalAlignment = CenterVertically) { - if (zapSender != null) { - if (showAuthorPicture) { - UserPicture(zapSender, Size25dp, accountViewModel = accountViewModel, nav = nav) - Spacer(HalfPadding) - } - UsernameDisplay(zapSender, Modifier.weight(1f, fill = false), textColor = textColor, accountViewModel = accountViewModel) - } else if (showAuthorPicture) { - NoteAuthorPicture(baseNote, Size25dp, accountViewModel = accountViewModel, nav = nav) + if (zapSender != null) { + if (showAuthorPicture) { + UserPicture(zapSender, Size25dp, accountViewModel = accountViewModel, nav = nav) Spacer(HalfPadding) - NoteUsernameDisplay(baseNote, Modifier.weight(1f, fill = false), textColor = textColor, accountViewModel = accountViewModel) - } else { - NoteUsernameDisplay(baseNote, Modifier.weight(1f, fill = false), textColor = textColor, accountViewModel = accountViewModel) } - - ActivityActionChip(baseNote, accountViewModel, nav) + UsernameDisplay(zapSender, Modifier.weight(1f), textColor = textColor, accountViewModel = accountViewModel) + } else if (showAuthorPicture) { + NoteAuthorPicture(baseNote, Size25dp, accountViewModel = accountViewModel, nav = nav) + Spacer(HalfPadding) + NoteUsernameDisplay(baseNote, Modifier.weight(1f), textColor = textColor, accountViewModel = accountViewModel) + } else { + NoteUsernameDisplay(baseNote, Modifier.weight(1f), textColor = textColor, accountViewModel = accountViewModel) } if (isDraft) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ActivityActionChip.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ActivityActionChip.kt deleted file mode 100644 index 736ea1dccd..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ActivityActionChip.kt +++ /dev/null @@ -1,167 +0,0 @@ -/* - * 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.types - -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp -import com.vitorpamplona.amethyst.commons.hashtags.Cashu -import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons -import com.vitorpamplona.amethyst.commons.icons.symbols.Icon -import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer -import com.vitorpamplona.amethyst.ui.navigation.navs.INav -import com.vitorpamplona.amethyst.ui.note.LikedIcon -import com.vitorpamplona.amethyst.ui.note.UserPicture -import com.vitorpamplona.amethyst.ui.note.ZapIcon -import com.vitorpamplona.amethyst.ui.note.showAmount -import com.vitorpamplona.amethyst.ui.note.showAmountInteger -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.theme.HalfPadding -import com.vitorpamplona.amethyst.ui.theme.Size16Modifier -import com.vitorpamplona.amethyst.ui.theme.Size19Modifier -import com.vitorpamplona.amethyst.ui.theme.Size20dp -import com.vitorpamplona.amethyst.ui.theme.bitcoinColor -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent -import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent -import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.claimedSatsTotal -import kotlinx.collections.immutable.persistentListOf -import java.math.BigDecimal -import androidx.compose.material3.Icon as Material3Icon - -/** - * Inline "activity" chip for the author header of reaction and zap cards: - * the action (emoji, or amount → recipient) renders next to the actor's name, - * so the card reads as "Alice ❤️" / "Alice ⚡ 500 → Bob" with the target post - * quoted below. Renders nothing for other kinds. - */ -@Composable -fun ActivityActionChip( - baseNote: Note, - accountViewModel: AccountViewModel, - nav: INav, -) { - when (val event = baseNote.event) { - is ReactionEvent -> { - Spacer(HalfPadding) - ReactionActionChip(event.content) - } - - is LnZapEvent -> { - Spacer(HalfPadding) - AmountToRecipientChip( - amount = showAmountInteger(event.amount), - recipient = event.zappedAuthor().firstOrNull(), - accountViewModel = accountViewModel, - nav = nav, - ) { - ZapIcon(Size19Modifier, MaterialTheme.colorScheme.bitcoinColor) - } - } - - is NutzapEvent -> { - Spacer(HalfPadding) - AmountToRecipientChip( - amount = showAmount(BigDecimal(event.claimedSatsTotal())), - recipient = event.linkedPubKeys().firstOrNull(), - accountViewModel = accountViewModel, - nav = nav, - ) { - Material3Icon( - imageVector = CustomHashTagIcons.Cashu, - contentDescription = null, - modifier = Size16Modifier, - ) - } - } - } -} - -@Composable -private fun ReactionActionChip(reactionType: String) { - if (reactionType.startsWith(":")) { - val url = reactionType.removePrefix(":").substringAfter(":") - InLineIconRenderer( - persistentListOf(CustomEmoji.ImageUrlType(url)), - style = SpanStyle(color = MaterialTheme.colorScheme.onBackground), - maxLines = 1, - fontSize = 16.sp, - ) - } else { - when (reactionType) { - "+" -> LikedIcon(Size19Modifier) - "-" -> - Text( - text = "👎", - maxLines = 1, - fontSize = 16.sp, - ) - - else -> - Text( - text = reactionType, - maxLines = 1, - fontSize = 16.sp, - ) - } - } -} - -@Composable -private fun AmountToRecipientChip( - amount: String, - recipient: HexKey?, - accountViewModel: AccountViewModel, - nav: INav, - icon: @Composable () -> Unit, -) { - Row(verticalAlignment = Alignment.CenterVertically) { - icon() - - Text( - text = amount, - color = MaterialTheme.colorScheme.bitcoinColor, - fontWeight = FontWeight.Bold, - fontSize = 14.sp, - maxLines = 1, - ) - - recipient?.let { - Icon( - symbol = MaterialSymbols.AutoMirrored.ArrowForwardIos, - contentDescription = null, - tint = MaterialTheme.colorScheme.bitcoinColor, - modifier = Size16Modifier, - ) - UserPicture(it, Size20dp, accountViewModel = accountViewModel, nav = nav) - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Nutzap.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Nutzap.kt index 5110dc88bc..ac8b5da929 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Nutzap.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Nutzap.kt @@ -20,23 +20,23 @@ */ package com.vitorpamplona.amethyst.ui.note.types -import androidx.compose.foundation.layout.Spacer import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.navigation.navs.INav -import com.vitorpamplona.amethyst.ui.note.CrossfadeToDisplayComment +import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification +import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent +import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.claimedSatsTotal +import java.math.BigDecimal /** - * Renders a NIP-61 nutzap (kind 9321) as an activity card, like lightning zaps. + * Renders a NIP-61 nutzap (kind 9321) as a transfer card, like lightning zaps. * Unlike kind 9735 receipts, the nutzap is signed by the sender, so [Note.author] - * is already the right person to attribute; the amount and recipient render in - * the card header (ActivityActionChip). The body is the comment, when present, - * over the quoted target post. + * is already the right person to attribute. */ @Composable fun RenderNutzap( @@ -48,10 +48,25 @@ fun RenderNutzap( ) { val nutzapEvent = note.event as? NutzapEvent ?: return - nutzapEvent.content.ifBlank { null }?.let { - CrossfadeToDisplayComment(it, backgroundColor, nav, accountViewModel) - Spacer(StdVertSpacer) - } + val recipientKey = nutzapEvent.linkedPubKeys().firstOrNull() ?: return - RenderActionTarget(note, quotesLeft, backgroundColor, accountViewModel, nav) + RenderZappedPost(note, quotesLeft, backgroundColor, accountViewModel, nav) + + val card = + remember(note) { + ZapAmountCommentNotification( + user = note.author, + comment = nutzapEvent.content.ifBlank { null }, + amount = showAmount(BigDecimal(nutzapEvent.claimedSatsTotal())), + zapNote = note, + ) + } + + TransferCard( + card, + recipientKey, + backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/OnchainZapEvent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/OnchainZapEvent.kt index d101001316..4c9104cb61 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/OnchainZapEvent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/OnchainZapEvent.kt @@ -99,7 +99,7 @@ fun RenderOnchainZap( val sender = note.author?.pubkeyHex ?: event.pubKey val recipient = event.recipient() ?: return - RenderActionTarget(note, quotesLeft, backgroundColor, accountViewModel, nav) + RenderZappedPost(note, quotesLeft, backgroundColor, accountViewModel, nav) val sats = event.claimedAmountInSats() ?: 0L val txid = event.txid() val message = event.content.takeIf { it.isNotBlank() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Reaction.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Reaction.kt index fa5cd29060..5ffd2f8b9a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Reaction.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Reaction.kt @@ -20,11 +20,14 @@ */ package com.vitorpamplona.amethyst.ui.note.types +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable @@ -35,7 +38,25 @@ fun RenderReaction( accountViewModel: AccountViewModel, nav: INav, ) { - // The reaction emoji renders in the card header (ActivityActionChip); - // the body is just the post being reacted to, framed as a quote. - RenderActionTarget(note, quotesLeft, backgroundColor, accountViewModel, nav) + note.replyTo?.lastOrNull()?.let { + NoteCompose( + it, + modifier = Modifier, + isBoostedNote = true, + makeItShort = true, + unPackReply = ReplyRenderType.NONE, + quotesLeft = quotesLeft - 1, + parentBackgroundColor = backgroundColor, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + // Reposts have trash in their contents. + val refactorReactionText = if (note.event?.content == "+") "❤" else note.event?.content ?: "" + + Text( + text = refactorReactionText, + maxLines = 1, + ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ZapEvent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ZapEvent.kt index 3dbdf9df54..8bc9b50955 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ZapEvent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ZapEvent.kt @@ -55,32 +55,30 @@ import com.vitorpamplona.amethyst.ui.theme.Size24Modifier import com.vitorpamplona.amethyst.ui.theme.Size25dp import com.vitorpamplona.amethyst.ui.theme.SpacedBy5dp import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer -import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.bitcoinColor -import com.vitorpamplona.amethyst.ui.theme.replyModifier import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext /** - * Shows the post a reaction or zap targets, framed as a quote so it reads as - * subordinate to the activity card around it. + * Shows the post a zap targets above the transfer card, mirroring how + * reactions and reposts embed their target. */ @Composable -fun RenderActionTarget( - actionNote: Note, +fun RenderZappedPost( + zapNote: Note, quotesLeft: Int, backgroundColor: MutableState, accountViewModel: AccountViewModel, nav: INav, ) { - actionNote.replyTo?.lastOrNull()?.let { + zapNote.replyTo?.lastOrNull()?.let { NoteCompose( it, - modifier = MaterialTheme.colorScheme.replyModifier, - isQuotedNote = true, + modifier = Modifier, + isBoostedNote = true, makeItShort = true, unPackReply = ReplyRenderType.NONE, quotesLeft = quotesLeft - 1, @@ -99,19 +97,22 @@ fun RenderLnZap( accountViewModel: AccountViewModel, nav: INav, ) { - if (note.event !is LnZapEvent) return + val zapEvent = note.event as? LnZapEvent ?: return + + RenderZappedPost(note, quotesLeft, backgroundColor, accountViewModel, nav) - // The amount and sender render in the card header (ActivityActionChip plus - // the zap-sender attribution); the body is the zap comment, when present, - // over the quoted target post. val card by parseAuthorCommentAndAmount(note, accountViewModel) - card.comment?.let { - CrossfadeToDisplayComment(it, backgroundColor, nav, accountViewModel) - Spacer(StdVertSpacer) - } + val destinationKey = zapEvent.zappedAuthor().firstOrNull() ?: return - RenderActionTarget(note, quotesLeft, backgroundColor, accountViewModel, nav) + TransferCard( + card, + destinationKey, + backgroundColor, + Modifier, + accountViewModel, + nav, + ) } /** diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index e158464b67..6174025d61 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -124,7 +124,6 @@ import com.vitorpamplona.amethyst.ui.note.elements.ShowForkInformation import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo import com.vitorpamplona.amethyst.ui.note.observeEdits import com.vitorpamplona.amethyst.ui.note.showAmount -import com.vitorpamplona.amethyst.ui.note.types.ActivityActionChip import com.vitorpamplona.amethyst.ui.note.types.AudioHeader import com.vitorpamplona.amethyst.ui.note.types.AudioTrackHeader import com.vitorpamplona.amethyst.ui.note.types.BadgeDisplay @@ -554,14 +553,10 @@ private fun FullBleedNoteCompose( Column(modifier = Modifier.padding(start = 10.dp)) { Row(verticalAlignment = Alignment.CenterVertically) { - Row(Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) { - if (zapSender != null) { - UsernameDisplay(zapSender, Modifier.weight(1f, fill = false), accountViewModel = accountViewModel) - } else { - NoteUsernameDisplay(baseNote, Modifier.weight(1f, fill = false), accountViewModel = accountViewModel) - } - - ActivityActionChip(baseNote, accountViewModel, nav) + if (zapSender != null) { + UsernameDisplay(zapSender, Modifier.weight(1f), accountViewModel = accountViewModel) + } else { + NoteUsernameDisplay(baseNote, Modifier.weight(1f), accountViewModel = accountViewModel) } if (isDraft) {