add zap buttons to PollNote options,
add poll_option tag and handlers to LnZapEvents and zap funs,
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user