diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index dee03ea65b..44475cfc7b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -422,7 +422,7 @@ data class ZapAmountCommentNotification( @Composable private fun ParseAuthorCommentAndAmount( zapRequest: Note, - zapEvent: Note?, + zapEvent: Note, accountViewModel: AccountViewModel, onReady: @Composable (MutableState) -> Unit, ) { @@ -437,7 +437,7 @@ private fun ParseAuthorCommentAndAmount( ) } - LaunchedEffect(key1 = zapRequest.idHex, key2 = zapEvent?.idHex) { + LaunchedEffect(key1 = zapRequest.idHex, key2 = zapEvent.idHex) { accountViewModel.decryptAmountMessage(zapRequest, zapEvent) { newState -> if (newState != null) { content.value = newState 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 9564d0d1b0..236497aef7 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 @@ -117,6 +117,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityChatMessage import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityEvent import com.vitorpamplona.amethyst.ui.note.types.RenderLiveChessChallenge import com.vitorpamplona.amethyst.ui.note.types.RenderLiveChessGameEnd +import com.vitorpamplona.amethyst.ui.note.types.RenderLnZap import com.vitorpamplona.amethyst.ui.note.types.RenderLongFormContent import com.vitorpamplona.amethyst.ui.note.types.RenderNIP90ContentDiscoveryResponse import com.vitorpamplona.amethyst.ui.note.types.RenderNIP90Status @@ -210,6 +211,7 @@ import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessa import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent +import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.splits.hasZapSplitSetup import com.vitorpamplona.quartz.nip58Badges.BadgeAwardEvent import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent @@ -793,6 +795,10 @@ private fun RenderNoteRow( RenderBadgeAward(baseNote, backgroundColor, accountViewModel, nav) } + is LnZapEvent -> { + RenderLnZap(baseNote, backgroundColor, accountViewModel, nav) + } + is FhirResourceEvent -> { RenderFhirResource(baseNote, accountViewModel, nav) } 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 new file mode 100644 index 0000000000..11ba6a8987 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ZapEvent.kt @@ -0,0 +1,185 @@ +/* + * 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.foundation.layout.fillMaxWidth +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowForwardIos +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.note.CrossfadeToDisplayComment +import com.vitorpamplona.amethyst.ui.note.UserPicture +import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification +import com.vitorpamplona.amethyst.ui.note.ZapIcon +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel +import com.vitorpamplona.amethyst.ui.theme.Size20Modifier +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.ThemeComparisonColumn +import com.vitorpamplona.amethyst.ui.theme.bitcoinColor +import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext + +@Composable +fun RenderLnZap( + note: Note, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, +) { + val zapEvent = note.event as? LnZapEvent ?: return + + val card by parseAuthorCommentAndAmount(note, accountViewModel) + + val destinationKey = zapEvent.zappedAuthor().firstOrNull() ?: return + + TransferCard( + card, + destinationKey, + backgroundColor, + Modifier, + accountViewModel, + nav, + ) +} + +@Composable +private fun parseAuthorCommentAndAmount( + zapEventNote: Note, + accountViewModel: AccountViewModel, +) = produceState( + ZapAmountCommentNotification( + user = null, + comment = null, + amount = null, + ), + zapEventNote, +) { + val newState = accountViewModel.innerDecryptAmountMessage(zapEventNote) + value = newState ?: ZapAmountCommentNotification( + user = null, + comment = null, + amount = null, + ) +} + +@Preview +@Composable +fun TransferCardPreview() { + var user1: User? = null + var user2: User? = null + + runBlocking { + withContext(Dispatchers.IO) { + user1 = LocalCache.getOrCreateUser("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c") + user2 = LocalCache.getOrCreateUser("ca89cb11f1c75d5b6622268ff43d2288ea8b2cb5b9aa996ff9ff704fc904b78b") + } + } + + ThemeComparisonColumn { + TransferCard( + transfer = + ZapAmountCommentNotification( + user = user1, + comment = "This is a comment", + amount = "100", + ), + toUser = user2!!.pubkeyHex, + backgroundColor = remember { mutableStateOf(Color.Transparent) }, + accountViewModel = mockAccountViewModel(), + nav = EmptyNav(), + ) + } +} + +@Composable +fun TransferCard( + transfer: ZapAmountCommentNotification, + toUser: String, + backgroundColor: MutableState, + modifier: Modifier = Modifier, + accountViewModel: AccountViewModel, + nav: INav, +) { + Row( + modifier = modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = SpacedBy5dp, + ) { + transfer.user?.let { + UserPicture(it, Size25dp, Modifier, accountViewModel, nav) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + ZapIcon( + Size24Modifier, + MaterialTheme.colorScheme.bitcoinColor, + ) + + // Amount Display + Text( + text = transfer.amount ?: "", + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.bitcoinColor, + ) + + Icon( + imageVector = Icons.AutoMirrored.Outlined.ArrowForwardIos, + contentDescription = null, + tint = MaterialTheme.colorScheme.bitcoinColor, + modifier = Size20Modifier, + ) + } + + UserPicture(toUser, Size25dp, Modifier, accountViewModel, nav) + + // Optional Message (conditionally displayed) + transfer.comment?.let { message -> + Spacer(StdHorzSpacer) + CrossfadeToDisplayComment(message, backgroundColor, accountViewModel = accountViewModel, nav = nav) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 647a4f905f..a014476d9c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -617,9 +617,14 @@ class AccountViewModel( val decryptedInfo = mapNotNullAsync(myList) { next -> - val info = innerDecryptAmountMessage(next.first, next.second) - if (info != null) { - DecryptedInfo(next.first, next.second, info) + val zap = next.second + if (zap != null) { + val info = innerDecryptAmountMessage(next.first, zap) + if (info != null) { + DecryptedInfo(next.first, zap, info) + } else { + null + } } else { null } @@ -633,7 +638,7 @@ class AccountViewModel( fun decryptAmountMessage( zapRequest: Note, - zapEvent: Note?, + zapEvent: Note, onNewState: (ZapAmountCommentNotification?) -> Unit, ) { viewModelScope.launch(Dispatchers.IO) { @@ -641,35 +646,50 @@ class AccountViewModel( } } - private suspend fun innerDecryptAmountMessage( + suspend fun innerDecryptAmountMessage(zapNote: Note): ZapAmountCommentNotification? { + val zapEvent = zapNote.event as? LnZapEvent ?: return null + val zapRequest = zapEvent.zapRequest ?: return null + + return innerDecryptAmountMessage(zapRequest, zapEvent) + } + + suspend fun innerDecryptAmountMessage( zapRequest: Note, - zapEvent: Note?, - ): ZapAmountCommentNotification? = - (zapRequest.event as? LnZapRequestEvent)?.let { - val amount = showAmountInteger((zapEvent?.event as? LnZapEvent)?.amount) - if (it.isPrivateZap()) { - val decryptedContent = account.decryptZapOrNull(it) - if (decryptedContent != null) { - ZapAmountCommentNotification( - LocalCache.checkGetOrCreateUser(decryptedContent.pubKey), - decryptedContent.content.ifBlank { null }, - amount, - ) - } else { - ZapAmountCommentNotification( - zapRequest.author, - null, - amount, - ) - } + zapEvent: Note, + ): ZapAmountCommentNotification? { + val zapEvent = zapEvent.event as? LnZapEvent ?: return null + val zapRequest = zapRequest.event as? LnZapRequestEvent ?: return null + return innerDecryptAmountMessage(zapRequest, zapEvent) + } + + suspend fun innerDecryptAmountMessage( + zapRequestEvent: LnZapRequestEvent, + zapEvent: LnZapEvent, + ): ZapAmountCommentNotification? { + val amount = showAmountInteger(zapEvent.amount) + return if (zapRequestEvent.isPrivateZap()) { + val decryptedContent = account.decryptZapOrNull(zapRequestEvent) + if (decryptedContent != null) { + ZapAmountCommentNotification( + LocalCache.checkGetOrCreateUser(decryptedContent.pubKey), + decryptedContent.content.ifBlank { null }, + amount, + ) } else { ZapAmountCommentNotification( - zapRequest.author, - zapRequest.event?.content?.ifBlank { null }, + account.cache.getOrCreateUser(zapRequestEvent.pubKey), + null, amount, ) } + } else { + ZapAmountCommentNotification( + account.cache.getOrCreateUser(zapRequestEvent.pubKey), + zapRequestEvent.content.ifBlank { null }, + amount, + ) } + } fun zap( note: Note, 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 9f64b6f304..49d4bf9540 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 @@ -147,6 +147,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderGitRepositoryEvent import com.vitorpamplona.amethyst.ui.note.types.RenderHighlight import com.vitorpamplona.amethyst.ui.note.types.RenderInteractiveStory import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityChatMessage +import com.vitorpamplona.amethyst.ui.note.types.RenderLnZap import com.vitorpamplona.amethyst.ui.note.types.RenderPinListEvent import com.vitorpamplona.amethyst.ui.note.types.RenderPoll import com.vitorpamplona.amethyst.ui.note.types.RenderPostApproval @@ -225,6 +226,7 @@ import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.splits.hasZapSplitSetup import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent @@ -607,6 +609,8 @@ private fun FullBleedNoteCompose( ) } else if (noteEvent is AdvertisedRelayListEvent) { DisplayNIP65RelayList(baseNote, backgroundColor, accountViewModel, nav) + } else if (noteEvent is LnZapEvent) { + RenderLnZap(baseNote, backgroundColor, accountViewModel, nav) } else if (noteEvent is SearchRelayListEvent) { DisplaySearchRelayList(baseNote, backgroundColor, accountViewModel, nav) } else if (noteEvent is BlockedRelayListEvent) {