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
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
@@ -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) }
@@ -6,6 +6,8 @@ interface LnZapEventInterface : EventInterface {
fun zappedPost(): List<String>
fun zappedPollOption(): Int?
fun zappedAuthor(): List<String>
fun taggedAddresses(): List<ATag>
@@ -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<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 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": <base64-encoded OTS file data> // TODO
}
*/
@@ -447,7 +447,7 @@ fun NoteCompose(
if (noteEvent is PollNoteEvent) {
PollNote(
noteEvent,
note,
canPreview = canPreview && !makeItShort,
backgroundColor,
accountViewModel,
@@ -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 = {}
)
)
}
}
}
}
}
@@ -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 = {
@@ -329,7 +329,7 @@ fun NoteMaster(
if (noteEvent is PollNoteEvent) {
PollNote(
noteEvent,
note,
canPreview,
backgroundColor,
accountViewModel,
@@ -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,