From 171a7841b39e5c790977e6f2a5fe2d3eb937f72f Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Thu, 23 Mar 2023 19:11:07 +0900 Subject: [PATCH] add zap buttons to PollNote options, add poll_option tag and handlers to LnZapEvents and zap funs, --- .../vitorpamplona/amethyst/model/Account.kt | 11 +- .../amethyst/service/model/LnZapEvent.kt | 4 + .../service/model/LnZapEventInterface.kt | 2 + .../service/model/LnZapRequestEvent.kt | 19 +- .../amethyst/ui/note/NoteCompose.kt | 2 +- .../amethyst/ui/note/PollNote.kt | 212 ++++++++++++++++-- .../amethyst/ui/note/ReactionsRow.kt | 6 +- .../amethyst/ui/screen/ThreadFeedView.kt | 2 +- .../ui/screen/loggedIn/AccountViewModel.kt | 4 +- 9 files changed, 233 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index ca7ae2b0e4..0daa97f587 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -124,11 +124,16 @@ class Account( } } - fun createZapRequestFor(note: Note): LnZapRequestEvent? { + fun createZapRequestFor(note: Note, pollOption: Int?): LnZapRequestEvent? { if (!isWriteable()) return null - note.event?.let { - return LnZapRequestEvent.create(it, userProfile().relays?.keys?.ifEmpty { null } ?: localRelays.map { it.url }.toSet(), loggedIn.privKey!!) + note.event?.let { event -> + return LnZapRequestEvent.create( + event, + userProfile().relays?.keys?.ifEmpty { null } ?: localRelays.map { it.url }.toSet(), + loggedIn.privKey!!, + pollOption + ) } return null diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt index b8845b8d54..a60118eaf3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt @@ -19,6 +19,10 @@ class LnZapEvent( .filter { it.firstOrNull() == "e" } .mapNotNull { it.getOrNull(1) } + override fun zappedPollOption(): Int? = tags + .filter { it.firstOrNull() == "poll_option" } + .getOrNull(1)?.getOrNull(1)?.toInt() + override fun zappedAuthor() = tags .filter { it.firstOrNull() == "p" } .mapNotNull { it.getOrNull(1) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEventInterface.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEventInterface.kt index cc95d34b32..c6ca7efb30 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEventInterface.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEventInterface.kt @@ -6,6 +6,8 @@ interface LnZapEventInterface : EventInterface { fun zappedPost(): List + fun zappedPollOption(): Int? + fun zappedAuthor(): List fun taggedAddresses(): List diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt index 75ce41ca29..4bf0f0d115 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt @@ -21,11 +21,19 @@ class LnZapRequestEvent( if (aTagValue != null) ATag.parse(aTagValue, relay) else null } + fun pollOption(): Int? = tags.filter { it.firstOrNull() == POLL_OPTION } + .getOrNull(1)?.getOrNull(1)?.toInt() companion object { const val kind = 9734 - fun create(originalNote: EventInterface, relays: Set, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent { + fun create( + originalNote: EventInterface, + relays: Set, + privateKey: ByteArray, + pollOption: Int?, + createdAt: Long = Date().time / 1000 + ): LnZapRequestEvent { val content = "" val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() var tags = listOf( @@ -36,6 +44,9 @@ class LnZapRequestEvent( if (originalNote is LongTextNoteEvent) { tags = tags + listOf(listOf("a", originalNote.address().toTag())) } + if (pollOption != null && pollOption >= 0) { + tags = tags + listOf(listOf(POLL_OPTION, pollOption.toString())) + } val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) @@ -85,7 +96,11 @@ class LnZapRequestEvent( "wss://nostr.bitcoiner.social", "ws://monad.jb55.com:8080", "wss://relay.snort.social" + ], + [ + "poll_option", "n" ] - ] + ], + "ots": // TODO } */ diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index fa40d1f7d1..6497fa261d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -447,7 +447,7 @@ fun NoteCompose( if (noteEvent is PollNoteEvent) { PollNote( - noteEvent, + note, canPreview = canPreview && !makeItShort, backgroundColor, accountViewModel, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt index 15f046029d..6211017ece 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt @@ -1,40 +1,218 @@ package com.vitorpamplona.amethyst.ui.note +import android.widget.Toast import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.border -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material.MaterialTheme -import androidx.compose.runtime.Composable +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Bolt +import androidx.compose.material.icons.outlined.Bolt +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.runtime.* +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Popup import androidx.navigation.NavController +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.PollNoteEvent import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange +import kotlinx.coroutines.launch @Composable fun PollNote( - pollEvent: PollNoteEvent, + note: Note, canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, navController: NavController ) { - val modifier = Modifier.fillMaxWidth() - .border(BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f))) - .padding(4.dp) + val pollEvent = note.event as PollNoteEvent pollEvent.pollOptions().forEach { poll_op -> - TranslateableRichTextViewer( - poll_op.value, - canPreview, - modifier, - pollEvent.tags(), - backgroundColor, - accountViewModel, - navController - ) + Row(Modifier.fillMaxWidth()) { + val modifier = Modifier + .weight(1f) + .border(BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f))) + .padding(4.dp) + + TranslateableRichTextViewer( + poll_op.value, + canPreview, + modifier, + pollEvent.tags(), + backgroundColor, + accountViewModel, + navController + ) + + ZapVote(note, accountViewModel, poll_op.key) + } + } +} + +@Composable +@OptIn(ExperimentalFoundationApi::class) +fun ZapVote( + baseNote: Note, + accountViewModel: AccountViewModel, + pollOption: Int, + modifier: Modifier = Modifier +) { + val accountState by accountViewModel.accountLiveData.observeAsState() + val account = accountState?.account ?: return + + val zapsState by baseNote.live().zaps.observeAsState() + val zappedNote = zapsState?.note + + var wantsToZap by remember { mutableStateOf(false) } + + val context = LocalContext.current + val scope = rememberCoroutineScope() + + Row( + modifier = Modifier + .then(Modifier.size(20.dp)) + .combinedClickable( + role = Role.Button, + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false, radius = 24.dp), + onClick = { + if (!accountViewModel.isWriteable()) { + scope.launch { + Toast + .makeText( + context, + context.getString(R.string.login_with_a_private_key_to_be_able_to_send_zaps), + Toast.LENGTH_SHORT + ) + .show() + } + } else if (account.zapAmountChoices.size == 1) { + accountViewModel.zap( + baseNote, + account.zapAmountChoices.first() * 1000, + pollOption, + "", + context + ) { + scope.launch { + Toast + .makeText(context, it, Toast.LENGTH_SHORT) + .show() + } + } + } else if (account.zapAmountChoices.size > 1) { + wantsToZap = true + } + }, + onLongClick = {} + ) + ) { + if (wantsToZap) { + ZapVoteAmountChoicePopup( + baseNote, + accountViewModel, + pollOption, + onDismiss = { + wantsToZap = false + }, + onError = { + scope.launch { + Toast.makeText(context, it, Toast.LENGTH_SHORT).show() + } + } + ) + } + + if (zappedNote?.isZappedBy(account.userProfile()) == true) { + Icon( + imageVector = Icons.Default.Bolt, + contentDescription = stringResource(R.string.zaps), + modifier = Modifier.size(20.dp), + tint = BitcoinOrange + ) + } else { + Icon( + imageVector = Icons.Outlined.Bolt, + contentDescription = stringResource(id = R.string.zaps), + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + } + } + + Text( + showAmount(zappedNote?.zappedAmount()), + fontSize = 14.sp, + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), + modifier = modifier + ) +} + +@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class) +@Composable +fun ZapVoteAmountChoicePopup( + baseNote: Note, + accountViewModel: AccountViewModel, + pollOption: Int, + onDismiss: () -> Unit, + onError: (text: String) -> Unit +) { + val context = LocalContext.current + + val accountState by accountViewModel.accountLiveData.observeAsState() + val account = accountState?.account ?: return + + Popup( + alignment = Alignment.BottomCenter, + offset = IntOffset(0, -50), + onDismissRequest = { onDismiss() } + ) { + FlowRow(horizontalArrangement = Arrangement.Center) { + account.zapAmountChoices.forEach { amountInSats -> + Button( + modifier = Modifier.padding(horizontal = 3.dp), + onClick = { + accountViewModel.zap(baseNote, amountInSats * 1000, pollOption, "", context, onError) + onDismiss() + }, + shape = RoundedCornerShape(20.dp), + colors = ButtonDefaults + .buttonColors( + backgroundColor = MaterialTheme.colors.primary + ) + ) { + Text( + "⚡ ${showAmount(amountInSats.toBigDecimal().setScale(1))}", + color = Color.White, + textAlign = TextAlign.Center, + modifier = Modifier.combinedClickable( + onClick = { + accountViewModel.zap(baseNote, amountInSats * 1000, pollOption, "", context, onError) + onDismiss() + }, + onLongClick = {} + ) + ) + } + } + } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index b955120a74..2bd06d01c4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -350,7 +350,7 @@ fun ZapReaction( .show() } } else if (account.zapAmountChoices.size == 1) { - accountViewModel.zap(baseNote, account.zapAmountChoices.first() * 1000, "", context) { + accountViewModel.zap(baseNote, account.zapAmountChoices.first() * 1000, null, "", context) { scope.launch { Toast .makeText(context, it, Toast.LENGTH_SHORT) @@ -502,7 +502,7 @@ fun ZapAmountChoicePopup(baseNote: Note, accountViewModel: AccountViewModel, onD Button( modifier = Modifier.padding(horizontal = 3.dp), onClick = { - accountViewModel.zap(baseNote, amountInSats * 1000, "", context, onError) + accountViewModel.zap(baseNote, amountInSats * 1000, null, "", context, onError) onDismiss() }, shape = RoundedCornerShape(20.dp), @@ -517,7 +517,7 @@ fun ZapAmountChoicePopup(baseNote: Note, accountViewModel: AccountViewModel, onD textAlign = TextAlign.Center, modifier = Modifier.combinedClickable( onClick = { - accountViewModel.zap(baseNote, amountInSats * 1000, "", context, onError) + accountViewModel.zap(baseNote, amountInSats * 1000, null, "", context, onError) onDismiss() }, onLongClick = { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index a9adaf72ee..7322ada1b0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -329,7 +329,7 @@ fun NoteMaster( if (noteEvent is PollNoteEvent) { PollNote( - noteEvent, + note, canPreview, backgroundColor, accountViewModel, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 2734c6ee19..50fc500ccf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -48,7 +48,7 @@ class AccountViewModel(private val account: Account) : ViewModel() { account.delete(account.boostsTo(note)) } - fun zap(note: Note, amount: Long, message: String, context: Context, onError: (String) -> Unit) { + fun zap(note: Note, amount: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit) { val lud16 = note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim() if (lud16.isNullOrBlank()) { @@ -56,7 +56,7 @@ class AccountViewModel(private val account: Account) : ViewModel() { return } - val zapRequest = account.createZapRequestFor(note) + val zapRequest = account.createZapRequestFor(note, pollOption) LightningAddressResolver().lnAddressInvoice( lud16,