add zap buttons to PollNote options,

add poll_option tag and handlers to LnZapEvents and zap funs,
This commit is contained in:
toadlyBroodle
2023-03-23 19:11:07 +09:00
parent 611dcc0189
commit 171a7841b3
9 changed files with 233 additions and 29 deletions
@@ -124,11 +124,16 @@ class Account(
} }
} }
fun createZapRequestFor(note: Note): LnZapRequestEvent? { fun createZapRequestFor(note: Note, pollOption: Int?): LnZapRequestEvent? {
if (!isWriteable()) return null if (!isWriteable()) return null
note.event?.let { note.event?.let { event ->
return LnZapRequestEvent.create(it, userProfile().relays?.keys?.ifEmpty { null } ?: localRelays.map { it.url }.toSet(), loggedIn.privKey!!) return LnZapRequestEvent.create(
event,
userProfile().relays?.keys?.ifEmpty { null } ?: localRelays.map { it.url }.toSet(),
loggedIn.privKey!!,
pollOption
)
} }
return null return null
@@ -19,6 +19,10 @@ class LnZapEvent(
.filter { it.firstOrNull() == "e" } .filter { it.firstOrNull() == "e" }
.mapNotNull { it.getOrNull(1) } .mapNotNull { it.getOrNull(1) }
override fun zappedPollOption(): Int? = tags
.filter { it.firstOrNull() == "poll_option" }
.getOrNull(1)?.getOrNull(1)?.toInt()
override fun zappedAuthor() = tags override fun zappedAuthor() = tags
.filter { it.firstOrNull() == "p" } .filter { it.firstOrNull() == "p" }
.mapNotNull { it.getOrNull(1) } .mapNotNull { it.getOrNull(1) }
@@ -6,6 +6,8 @@ interface LnZapEventInterface : EventInterface {
fun zappedPost(): List<String> fun zappedPost(): List<String>
fun zappedPollOption(): Int?
fun zappedAuthor(): List<String> fun zappedAuthor(): List<String>
fun taggedAddresses(): List<ATag> fun taggedAddresses(): List<ATag>
@@ -21,11 +21,19 @@ class LnZapRequestEvent(
if (aTagValue != null) ATag.parse(aTagValue, relay) else null 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 { companion object {
const val kind = 9734 const val kind = 9734
fun create(originalNote: EventInterface, relays: Set<String>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent { fun create(
originalNote: EventInterface,
relays: Set<String>,
privateKey: ByteArray,
pollOption: Int?,
createdAt: Long = Date().time / 1000
): LnZapRequestEvent {
val content = "" val content = ""
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
var tags = listOf( var tags = listOf(
@@ -36,6 +44,9 @@ class LnZapRequestEvent(
if (originalNote is LongTextNoteEvent) { if (originalNote is LongTextNoteEvent) {
tags = tags + listOf(listOf("a", originalNote.address().toTag())) 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 id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey) val sig = Utils.sign(id, privateKey)
@@ -85,7 +96,11 @@ class LnZapRequestEvent(
"wss://nostr.bitcoiner.social", "wss://nostr.bitcoiner.social",
"ws://monad.jb55.com:8080", "ws://monad.jb55.com:8080",
"wss://relay.snort.social" "wss://relay.snort.social"
],
[
"poll_option", "n"
] ]
] ],
"ots": <base64-encoded OTS file data> // TODO
} }
*/ */
@@ -447,7 +447,7 @@ fun NoteCompose(
if (noteEvent is PollNoteEvent) { if (noteEvent is PollNoteEvent) {
PollNote( PollNote(
noteEvent, note,
canPreview = canPreview && !makeItShort, canPreview = canPreview && !makeItShort,
backgroundColor, backgroundColor,
accountViewModel, accountViewModel,
@@ -1,40 +1,218 @@
package com.vitorpamplona.amethyst.ui.note package com.vitorpamplona.amethyst.ui.note
import android.widget.Toast
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material.MaterialTheme import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable 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.Modifier
import androidx.compose.ui.graphics.Color 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.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Popup
import androidx.navigation.NavController 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.service.model.PollNoteEvent
import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import kotlinx.coroutines.launch
@Composable @Composable
fun PollNote( fun PollNote(
pollEvent: PollNoteEvent, note: Note,
canPreview: Boolean, canPreview: Boolean,
backgroundColor: Color, backgroundColor: Color,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
navController: NavController navController: NavController
) { ) {
val modifier = Modifier.fillMaxWidth() val pollEvent = note.event as PollNoteEvent
.border(BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f)))
.padding(4.dp)
pollEvent.pollOptions().forEach { poll_op -> pollEvent.pollOptions().forEach { poll_op ->
TranslateableRichTextViewer( Row(Modifier.fillMaxWidth()) {
poll_op.value, val modifier = Modifier
canPreview, .weight(1f)
modifier, .border(BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f)))
pollEvent.tags(), .padding(4.dp)
backgroundColor,
accountViewModel, TranslateableRichTextViewer(
navController 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 = {}
)
)
}
}
}
} }
} }
@@ -350,7 +350,7 @@ fun ZapReaction(
.show() .show()
} }
} else if (account.zapAmountChoices.size == 1) { } 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 { scope.launch {
Toast Toast
.makeText(context, it, Toast.LENGTH_SHORT) .makeText(context, it, Toast.LENGTH_SHORT)
@@ -502,7 +502,7 @@ fun ZapAmountChoicePopup(baseNote: Note, accountViewModel: AccountViewModel, onD
Button( Button(
modifier = Modifier.padding(horizontal = 3.dp), modifier = Modifier.padding(horizontal = 3.dp),
onClick = { onClick = {
accountViewModel.zap(baseNote, amountInSats * 1000, "", context, onError) accountViewModel.zap(baseNote, amountInSats * 1000, null, "", context, onError)
onDismiss() onDismiss()
}, },
shape = RoundedCornerShape(20.dp), shape = RoundedCornerShape(20.dp),
@@ -517,7 +517,7 @@ fun ZapAmountChoicePopup(baseNote: Note, accountViewModel: AccountViewModel, onD
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.combinedClickable( modifier = Modifier.combinedClickable(
onClick = { onClick = {
accountViewModel.zap(baseNote, amountInSats * 1000, "", context, onError) accountViewModel.zap(baseNote, amountInSats * 1000, null, "", context, onError)
onDismiss() onDismiss()
}, },
onLongClick = { onLongClick = {
@@ -329,7 +329,7 @@ fun NoteMaster(
if (noteEvent is PollNoteEvent) { if (noteEvent is PollNoteEvent) {
PollNote( PollNote(
noteEvent, note,
canPreview, canPreview,
backgroundColor, backgroundColor,
accountViewModel, accountViewModel,
@@ -48,7 +48,7 @@ class AccountViewModel(private val account: Account) : ViewModel() {
account.delete(account.boostsTo(note)) 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() val lud16 = note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim()
if (lud16.isNullOrBlank()) { if (lud16.isNullOrBlank()) {
@@ -56,7 +56,7 @@ class AccountViewModel(private val account: Account) : ViewModel() {
return return
} }
val zapRequest = account.createZapRequestFor(note) val zapRequest = account.createZapRequestFor(note, pollOption)
LightningAddressResolver().lnAddressInvoice( LightningAddressResolver().lnAddressInvoice(
lud16, lud16,