Adds zap splits

This commit is contained in:
Vitor Pamplona
2023-09-15 12:56:24 -04:00
parent 7bee44c5ad
commit 20e76daff8
20 changed files with 533 additions and 85 deletions
@@ -280,7 +280,7 @@ class Account(
} }
} }
fun createZapRequestFor(note: Note, pollOption: Int?, message: String = "", zapType: LnZapEvent.ZapType): LnZapRequestEvent? { fun createZapRequestFor(note: Note, pollOption: Int?, message: String = "", zapType: LnZapEvent.ZapType, toUser: User?): LnZapRequestEvent? {
if (!isWriteable()) return null if (!isWriteable()) return null
note.event?.let { event -> note.event?.let { event ->
@@ -291,7 +291,8 @@ class Account(
keyPair.privKey!!, keyPair.privKey!!,
pollOption, pollOption,
message, message,
zapType zapType,
toUser?.pubkeyHex
) )
} }
return null return null
@@ -759,7 +760,7 @@ class Account(
replyTo: List<Note>?, replyTo: List<Note>?,
mentions: List<User>?, mentions: List<User>?,
tags: List<String>? = null, tags: List<String>? = null,
zapReceiver: String? = null, zapReceiver: List<ZapSplitSetup>? = null,
wantsToMarkAsSensitive: Boolean, wantsToMarkAsSensitive: Boolean,
zapRaiserAmount: Long? = null, zapRaiserAmount: Long? = null,
replyingTo: String?, replyingTo: String?,
@@ -820,7 +821,7 @@ class Account(
valueMinimum: Int?, valueMinimum: Int?,
consensusThreshold: Int?, consensusThreshold: Int?,
closedAt: Int?, closedAt: Int?,
zapReceiver: String? = null, zapReceiver: List<ZapSplitSetup>? = null,
wantsToMarkAsSensitive: Boolean, wantsToMarkAsSensitive: Boolean,
zapRaiserAmount: Long? = null, zapRaiserAmount: Long? = null,
relayList: List<Relay>? = null, relayList: List<Relay>? = null,
@@ -864,7 +865,7 @@ class Account(
} }
} }
fun sendChannelMessage(message: String, toChannel: String, replyTo: List<Note>?, mentions: List<User>?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null, geohash: String? = null) { fun sendChannelMessage(message: String, toChannel: String, replyTo: List<Note>?, mentions: List<User>?, zapReceiver: List<ZapSplitSetup>? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null, geohash: String? = null) {
if (!isWriteable()) return if (!isWriteable()) return
// val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null } // val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null }
@@ -886,7 +887,7 @@ class Account(
LocalCache.consume(signedEvent, null) LocalCache.consume(signedEvent, null)
} }
fun sendLiveMessage(message: String, toChannel: ATag, replyTo: List<Note>?, mentions: List<User>?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null, geohash: String? = null) { fun sendLiveMessage(message: String, toChannel: ATag, replyTo: List<Note>?, mentions: List<User>?, zapReceiver: List<ZapSplitSetup>? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null, geohash: String? = null) {
if (!isWriteable()) return if (!isWriteable()) return
// val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null } // val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null }
@@ -908,11 +909,11 @@ class Account(
LocalCache.consume(signedEvent, null) LocalCache.consume(signedEvent, null)
} }
fun sendPrivateMessage(message: String, toUser: User, replyingTo: Note? = null, mentions: List<User>?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null, geohash: String? = null) { fun sendPrivateMessage(message: String, toUser: User, replyingTo: Note? = null, mentions: List<User>?, zapReceiver: List<ZapSplitSetup>? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null, geohash: String? = null) {
sendPrivateMessage(message, toUser.pubkeyHex, replyingTo, mentions, zapReceiver, wantsToMarkAsSensitive, zapRaiserAmount, geohash) sendPrivateMessage(message, toUser.pubkeyHex, replyingTo, mentions, zapReceiver, wantsToMarkAsSensitive, zapRaiserAmount, geohash)
} }
fun sendPrivateMessage(message: String, toUser: HexKey, replyingTo: Note? = null, mentions: List<User>?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null, geohash: String? = null) { fun sendPrivateMessage(message: String, toUser: HexKey, replyingTo: Note? = null, mentions: List<User>?, zapReceiver: List<ZapSplitSetup>? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null, geohash: String? = null) {
if (!isWriteable()) return if (!isWriteable()) return
val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null } val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null }
@@ -941,7 +942,7 @@ class Account(
subject: String? = null, subject: String? = null,
replyingTo: Note? = null, replyingTo: Note? = null,
mentions: List<User>?, mentions: List<User>?,
zapReceiver: String? = null, zapReceiver: List<ZapSplitSetup>? = null,
wantsToMarkAsSensitive: Boolean, wantsToMarkAsSensitive: Boolean,
zapRaiserAmount: Long? = null, zapRaiserAmount: Long? = null,
geohash: String? = null geohash: String? = null
@@ -9,11 +9,9 @@ import com.vitorpamplona.quartz.encoders.Lud06
import com.vitorpamplona.quartz.encoders.toLnUrl import com.vitorpamplona.quartz.encoders.toLnUrl
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Request import okhttp3.Request
import okhttp3.Response
import java.math.BigDecimal import java.math.BigDecimal
import java.math.RoundingMode
import java.net.URLEncoder import java.net.URLEncoder
class LightningAddressResolver() { class LightningAddressResolver() {
@@ -62,7 +60,7 @@ class LightningAddressResolver() {
} }
} }
suspend fun fetchLightningInvoice(lnCallback: String, milliSats: Long, message: String, nostrRequest: String? = null, onSuccess: (String) -> Unit, onError: (String) -> Unit) = withContext(Dispatchers.IO) { suspend fun fetchLightningInvoice(lnCallback: String, milliSats: Long, message: String, nostrRequest: String? = null, onSuccess: suspend (String) -> Unit, onError: (String) -> Unit) = withContext(Dispatchers.IO) {
val encodedMessage = URLEncoder.encode(message, "utf-8") val encodedMessage = URLEncoder.encode(message, "utf-8")
val urlBinder = if (lnCallback.contains("?")) "&" else "?" val urlBinder = if (lnCallback.contains("?")) "&" else "?"
@@ -78,24 +76,15 @@ class LightningAddressResolver() {
.url(url) .url(url)
.build() .build()
client.newCall(request).enqueue(object : Callback { client.newCall(request).execute().use {
override fun onResponse(call: Call, response: Response) {
response.use {
if (it.isSuccessful) { if (it.isSuccessful) {
onSuccess(response.body.string()) onSuccess(it.body.string())
} else { } else {
onError("Could not fetch invoice from $lnCallback") onError("Could not fetch invoice from $lnCallback")
} }
} }
} }
override fun onFailure(call: Call, e: java.io.IOException) {
onError("Could not fetch an invoice from $lnCallback. Message ${e.message}")
e.printStackTrace()
}
})
}
suspend fun lnAddressToLnUrl(lnaddress: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) { suspend fun lnAddressToLnUrl(lnaddress: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
fetchLightningAddressJson( fetchLightningAddressJson(
lnaddress, lnaddress,
@@ -111,7 +100,7 @@ class LightningAddressResolver() {
milliSats: Long, milliSats: Long,
message: String, message: String,
nostrRequest: String? = null, nostrRequest: String? = null,
onSuccess: (String) -> Unit, onSuccess: suspend (String) -> Unit,
onError: (String) -> Unit, onError: (String) -> Unit,
onProgress: (percent: Float) -> Unit onProgress: (percent: Float) -> Unit
) { ) {
@@ -155,13 +144,14 @@ class LightningAddressResolver() {
lnInvoice?.get("pr")?.asText()?.ifBlank { null }?.let { pr -> lnInvoice?.get("pr")?.asText()?.ifBlank { null }?.let { pr ->
// Forces LN Invoice amount to be the requested amount. // Forces LN Invoice amount to be the requested amount.
val expectedAmountInSats = BigDecimal(milliSats).divide(BigDecimal(1000), RoundingMode.HALF_UP).toLong()
val invoiceAmount = LnInvoiceUtil.getAmountInSats(pr) val invoiceAmount = LnInvoiceUtil.getAmountInSats(pr)
if (invoiceAmount.multiply(BigDecimal(1000)).toLong() == BigDecimal(milliSats).toLong()) { if (invoiceAmount.toLong() == expectedAmountInSats) {
onProgress(0.7f) onProgress(0.7f)
onSuccess(pr) onSuccess(pr)
} else { } else {
onProgress(0.0f) onProgress(0.0f)
onError("Incorrect invoice amount (${invoiceAmount.toLong()} sats) from server") onError("Incorrect invoice amount (${invoiceAmount.toLong()} sats) from ${lnaddress}. It should have been ${expectedAmountInSats}")
} }
} ?: lnInvoice?.get("reason")?.asText()?.ifBlank { null }?.let { reason -> } ?: lnInvoice?.get("reason")?.asText()?.ifBlank { null }?.let { reason ->
onProgress(0.0f) onProgress(0.0f)
@@ -9,6 +9,7 @@ import android.util.Size
import android.widget.Toast import android.widget.Toast
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@@ -82,20 +83,24 @@ import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
import com.vitorpamplona.amethyst.service.ReverseGeoLocationUtil import com.vitorpamplona.amethyst.service.ReverseGeoLocationUtil
import com.vitorpamplona.amethyst.service.noProtocolUrlValidator import com.vitorpamplona.amethyst.service.noProtocolUrlValidator
import com.vitorpamplona.amethyst.ui.components.* import com.vitorpamplona.amethyst.ui.components.*
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
import com.vitorpamplona.amethyst.ui.note.CancelIcon import com.vitorpamplona.amethyst.ui.note.CancelIcon
import com.vitorpamplona.amethyst.ui.note.CloseIcon import com.vitorpamplona.amethyst.ui.note.CloseIcon
import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.note.PollIcon import com.vitorpamplona.amethyst.ui.note.PollIcon
import com.vitorpamplona.amethyst.ui.note.RegularPostIcon import com.vitorpamplona.amethyst.ui.note.RegularPostIcon
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.MyTextField import com.vitorpamplona.amethyst.ui.screen.loggedIn.MyTextField
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.Font14SP import com.vitorpamplona.amethyst.ui.theme.Font14SP
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size55dp
import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.Size5dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
@@ -111,6 +116,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.lang.Math.round
@Composable @Composable
fun NewPostView( fun NewPostView(
@@ -321,9 +327,9 @@ fun NewPostView(
if (postViewModel.wantsForwardZapTo) { if (postViewModel.wantsForwardZapTo) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp) modifier = Modifier.padding(top = Size5dp, bottom = Size5dp, start = Size10dp)
) { ) {
FowardZapTo(postViewModel) FowardZapTo(postViewModel, accountViewModel)
} }
} }
@@ -721,7 +727,7 @@ fun SendDirectMessageTo(postViewModel: NewPostViewModel) {
} }
@Composable @Composable
fun FowardZapTo(postViewModel: NewPostViewModel) { fun FowardZapTo(postViewModel: NewPostViewModel, accountViewModel: AccountViewModel) {
Column( Column(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
@@ -755,7 +761,7 @@ fun FowardZapTo(postViewModel: NewPostViewModel) {
} }
Text( Text(
text = stringResource(R.string.zap_forward_title), text = stringResource(R.string.zap_split_title),
fontSize = 20.sp, fontSize = 20.sp,
fontWeight = FontWeight.W500, fontWeight = FontWeight.W500,
modifier = Modifier.padding(start = 10.dp) modifier = Modifier.padding(start = 10.dp)
@@ -765,22 +771,52 @@ fun FowardZapTo(postViewModel: NewPostViewModel) {
Divider() Divider()
Text( Text(
text = stringResource(R.string.zap_forward_explainer), text = stringResource(R.string.zap_split_explainer),
color = MaterialTheme.colors.placeholderText, color = MaterialTheme.colors.placeholderText,
modifier = Modifier.padding(vertical = 10.dp) modifier = Modifier.padding(vertical = 10.dp)
) )
postViewModel.forwardZapTo.items.forEachIndexed { index, splitItem ->
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = Size10dp)) {
BaseUserPicture(splitItem.key, Size55dp, accountViewModel = accountViewModel)
Spacer(modifier = DoubleHorzSpacer)
Column(modifier = Modifier.weight(1f)) {
UsernameDisplay(splitItem.key, showPlayButton = false)
Text(
text = String.format("%.0f%%", splitItem.percentage * 100),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
fontWeight = FontWeight.Bold,
fontSize = 18.sp
)
}
Spacer(modifier = DoubleHorzSpacer)
Slider(
value = splitItem.percentage,
onValueChange = { sliderValue ->
val rounded = (round(sliderValue * 20)) / 20.0f
postViewModel.updateZapPercentage(index, rounded)
},
modifier = Modifier
.weight(1.5f)
)
}
}
OutlinedTextField( OutlinedTextField(
value = postViewModel.forwardZapToEditting, value = postViewModel.forwardZapToEditting,
onValueChange = { onValueChange = {
postViewModel.updateZapForwardTo(it) postViewModel.updateZapForwardTo(it)
}, },
label = { Text(text = stringResource(R.string.zap_split_serarch_and_add_user)) },
label = { Text(text = stringResource(R.string.zap_forward_lnAddress)) },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
placeholder = { placeholder = {
Text( Text(
text = stringResource(R.string.zap_forward_lnAddress), text = stringResource(R.string.zap_split_serarch_and_add_user_placeholder),
color = MaterialTheme.colors.placeholderText color = MaterialTheme.colors.placeholderText
) )
}, },
@@ -21,6 +21,7 @@ import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
import com.vitorpamplona.amethyst.service.noProtocolUrlValidator import com.vitorpamplona.amethyst.service.noProtocolUrlValidator
import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.components.MediaCompressor import com.vitorpamplona.amethyst.ui.components.MediaCompressor
import com.vitorpamplona.amethyst.ui.components.Split
import com.vitorpamplona.amethyst.ui.components.isValidURL import com.vitorpamplona.amethyst.ui.components.isValidURL
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.events.AddressableEvent import com.vitorpamplona.quartz.events.AddressableEvent
@@ -29,6 +30,7 @@ import com.vitorpamplona.quartz.events.ChatMessageEvent
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.PrivateDmEvent import com.vitorpamplona.quartz.events.PrivateDmEvent
import com.vitorpamplona.quartz.events.TextNoteEvent import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.ZapSplitSetup
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@@ -88,9 +90,13 @@ open class NewPostViewModel() : ViewModel() {
var canAddInvoice by mutableStateOf(false) var canAddInvoice by mutableStateOf(false)
var wantsInvoice by mutableStateOf(false) var wantsInvoice by mutableStateOf(false)
data class ForwardZapSetup(val user: User) {
var percentage by mutableStateOf(100)
}
// Forward Zap to // Forward Zap to
var wantsForwardZapTo by mutableStateOf(false) var wantsForwardZapTo by mutableStateOf(false)
var forwardZapTo by mutableStateOf<User?>(null) var forwardZapTo by mutableStateOf<Split<User>>(Split())
var forwardZapToEditting by mutableStateOf(TextFieldValue("")) var forwardZapToEditting by mutableStateOf(TextFieldValue(""))
// NSFW, Sensitive // NSFW, Sensitive
@@ -151,7 +157,7 @@ open class NewPostViewModel() : ViewModel() {
wantsToAddGeoHash = false wantsToAddGeoHash = false
wantsZapraiser = false wantsZapraiser = false
zapRaiserAmount = null zapRaiserAmount = null
forwardZapTo = null forwardZapTo = Split()
forwardZapToEditting = TextFieldValue("") forwardZapToEditting = TextFieldValue("")
this.account = account this.account = account
@@ -166,10 +172,13 @@ open class NewPostViewModel() : ViewModel() {
val dmUsers = toUsersTagger.mentions val dmUsers = toUsersTagger.mentions
val zapReceiver = if (wantsForwardZapTo) { val zapReceiver = if (wantsForwardZapTo) {
if (forwardZapTo != null) { forwardZapTo?.items?.map {
forwardZapTo?.info?.lud16 ?: forwardZapTo?.info?.lud06 ZapSplitSetup(
} else { lnAddressOrPubKeyHex = it.key.pubkeyHex,
forwardZapToEditting.text relay = it.key.relaysBeingUsed.keys.firstOrNull(),
weight = it.percentage.toDouble(),
isLnAddress = false
)
} }
} else { } else {
null null
@@ -355,7 +364,7 @@ open class NewPostViewModel() : ViewModel() {
wantsForwardZapTo = false wantsForwardZapTo = false
wantsToMarkAsSensitive = false wantsToMarkAsSensitive = false
wantsToAddGeoHash = false wantsToAddGeoHash = false
forwardZapTo = null forwardZapTo = Split()
forwardZapToEditting = TextFieldValue("") forwardZapToEditting = TextFieldValue("")
userSuggestions = emptyList() userSuggestions = emptyList()
@@ -460,6 +469,9 @@ open class NewPostViewModel() : ViewModel() {
TextRange(lastWordStart + wordToInsert.length, lastWordStart + wordToInsert.length) TextRange(lastWordStart + wordToInsert.length, lastWordStart + wordToInsert.length)
) )
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) { } else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
forwardZapTo?.addItem(item)
forwardZapToEditting = TextFieldValue("")
/*
val lastWord = forwardZapToEditting.text.substring(0, it.end).substringAfterLast("\n").substringAfterLast(" ") val lastWord = forwardZapToEditting.text.substring(0, it.end).substringAfterLast("\n").substringAfterLast(" ")
val lastWordStart = it.end - lastWord.length val lastWordStart = it.end - lastWord.length
val wordToInsert = "@${item.pubkeyNpub()}" val wordToInsert = "@${item.pubkeyNpub()}"
@@ -468,7 +480,7 @@ open class NewPostViewModel() : ViewModel() {
forwardZapToEditting = TextFieldValue( forwardZapToEditting = TextFieldValue(
forwardZapToEditting.text.replaceRange(lastWordStart, it.end, wordToInsert), forwardZapToEditting.text.replaceRange(lastWordStart, it.end, wordToInsert),
TextRange(lastWordStart + wordToInsert.length, lastWordStart + wordToInsert.length) TextRange(lastWordStart + wordToInsert.length, lastWordStart + wordToInsert.length)
) )*/
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.TO_USERS) { } else if (userSuggestionsMainMessage == UserSuggestionAnchor.TO_USERS) {
val lastWord = toUsers.text.substring(0, it.end).substringAfterLast("\n").substringAfterLast(" ") val lastWord = toUsers.text.substring(0, it.end).substringAfterLast("\n").substringAfterLast(" ")
val lastWordStart = it.end - lastWord.length val lastWordStart = it.end - lastWord.length
@@ -656,6 +668,10 @@ open class NewPostViewModel() : ViewModel() {
isValidvalueMaximum.value = true isValidvalueMaximum.value = true
} }
} }
fun updateZapPercentage(index: Int, sliderValue: Float) {
forwardZapTo?.updatePercentage(index, sliderValue)
}
} }
enum class GeohashPrecision(val digits: Int) { enum class GeohashPrecision(val digits: Int) {
@@ -0,0 +1,99 @@
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import kotlin.math.abs
class SplitItem<T>(val key: T) {
var percentage by mutableStateOf(0f)
}
class Split<T>() {
var items: List<SplitItem<T>> by mutableStateOf(emptyList())
fun addItem(key: T): Int {
val wasEqualSplit = isEqualSplit()
val newItem = SplitItem(key)
items = items.plus(newItem)
if (wasEqualSplit) {
forceEqualSplit()
} else {
updatePercentage(items.lastIndex, equalSplit())
}
return items.lastIndex
}
fun equalSplit() = 1f / items.size
fun isEqualSplit(): Boolean {
val expectedPercentage = equalSplit()
return items.all { (it.percentage - expectedPercentage) < 0.01 }
}
fun forceEqualSplit() {
val correctPercentage = equalSplit()
items.forEach {
it.percentage = correctPercentage
}
}
fun updatePercentage(index: Int, percentage: Float) {
if (items.isEmpty()) return
val splitItem = items.getOrNull(index) ?: return
if (items.size == 1) {
splitItem.percentage = 1f
} else {
splitItem.percentage = percentage
println("Update ${items[index].key} to $percentage")
val othersMustShare = 1.0f - splitItem.percentage
val othersHave = items.sumOf {
if (it == splitItem) 0.0 else it.percentage.toDouble()
}.toFloat()
if (abs(othersHave - othersMustShare) < 0.01) return // nothing to do
println("Others Must Share $othersMustShare but have $othersHave")
bottomUpAdjustment(othersMustShare, othersHave, index)
}
}
private fun bottomUpAdjustment(othersMustShare: Float, othersHave: Float, exceptForIndex: Int) {
var needToRemove = othersHave - othersMustShare
if (needToRemove > 0) {
for (i in items.indices.reversed()) {
if (i == exceptForIndex) continue // do not update the current item
if (needToRemove < items[i].percentage) {
val oldValue = items[i].percentage
items[i].percentage -= needToRemove
needToRemove = 0f
println("- Updating ${items[i].key} from $oldValue to ${items[i].percentage - needToRemove}. $needToRemove left")
} else {
val oldValue = items[i].percentage
needToRemove -= items[i].percentage
items[i].percentage = 0f
println("- Updating ${items[i].key} from $oldValue to ${0}. $needToRemove left")
}
if (needToRemove < 0.01) {
break
}
}
} else if (needToRemove < 0) {
if (items.lastIndex == exceptForIndex) {
items[items.lastIndex - 1].percentage += -needToRemove
} else {
items.last().percentage += -needToRemove
}
}
}
}
@@ -31,11 +31,15 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Button import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Divider import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.IconButton import androidx.compose.material.IconButton
import androidx.compose.material.LocalTextStyle import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.darkColors import androidx.compose.material.darkColors
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowForwardIos
import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.material.lightColors import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -117,11 +121,13 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.LeaveCommunityButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.NormalTimeAgo import com.vitorpamplona.amethyst.ui.screen.loggedIn.NormalTimeAgo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.Font14SP import com.vitorpamplona.amethyst.ui.theme.Font14SP
import com.vitorpamplona.amethyst.ui.theme.HalfDoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.HalfPadding import com.vitorpamplona.amethyst.ui.theme.HalfPadding
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
@@ -173,6 +179,7 @@ import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.events.EmojiPackEvent import com.vitorpamplona.quartz.events.EmojiPackEvent
import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent
import com.vitorpamplona.quartz.events.EmojiUrl import com.vitorpamplona.quartz.events.EmojiUrl
import com.vitorpamplona.quartz.events.EventInterface
import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileHeaderEvent
import com.vitorpamplona.quartz.events.FileStorageHeaderEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent
import com.vitorpamplona.quartz.events.GenericRepostEvent import com.vitorpamplona.quartz.events.GenericRepostEvent
@@ -1055,6 +1062,60 @@ private fun NoteBody(
accountViewModel, accountViewModel,
nav nav
) )
val noteEvent = baseNote.event
val zapSplits = remember(noteEvent) { noteEvent?.hasZapSplitSetup() ?: false }
if (zapSplits && noteEvent != null) {
Spacer(modifier = HalfDoubleVertSpacer)
DisplayZapSplits(noteEvent, accountViewModel, nav)
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun DisplayZapSplits(noteEvent: EventInterface, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val list = remember(noteEvent) { noteEvent.zapSplitSetup() }
Row(verticalAlignment = CenterVertically) {
Box(
Modifier
.height(20.dp)
.width(25.dp)
) {
Icon(
imageVector = Icons.Outlined.Bolt,
contentDescription = stringResource(id = R.string.zaps),
modifier = Modifier
.size(20.dp)
.align(Alignment.CenterStart),
tint = BitcoinOrange
)
Icon(
imageVector = Icons.Outlined.ArrowForwardIos,
contentDescription = stringResource(id = R.string.zaps),
modifier = Modifier
.size(13.dp)
.align(Alignment.CenterEnd),
tint = BitcoinOrange
)
}
Spacer(modifier = StdHorzSpacer)
FlowRow {
list.forEach {
if (it.isLnAddress) {
ClickableText(
text = AnnotatedString(it.lnAddressOrPubKeyHex),
onClick = { },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
)
} else {
UserPicture(userHex = it.lnAddressOrPubKeyHex, size = 25.dp, accountViewModel = accountViewModel, nav = nav)
}
}
}
}
} }
@Composable @Composable
@@ -102,6 +102,32 @@ fun DisplayBlankAuthor(size: Dp, modifier: Modifier = Modifier) {
) )
} }
@Composable
fun UserPicture(
userHex: String,
size: Dp,
pictureModifier: Modifier = remember { Modifier },
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
LoadUser(baseUserHex = userHex) {
if (it != null) {
UserPicture(
user = it,
size = size,
pictureModifier = pictureModifier,
accountViewModel = accountViewModel,
nav = nav
)
} else {
DisplayBlankAuthor(
size,
pictureModifier
)
}
}
}
@Composable @Composable
fun UserPicture( fun UserPicture(
user: User, user: User,
@@ -41,6 +41,7 @@ import com.vitorpamplona.quartz.events.PayInvoiceErrorResponse
import com.vitorpamplona.quartz.events.ReportEvent import com.vitorpamplona.quartz.events.ReportEvent
import com.vitorpamplona.quartz.events.SealedGossipEvent import com.vitorpamplona.quartz.events.SealedGossipEvent
import com.vitorpamplona.quartz.events.UserMetadata import com.vitorpamplona.quartz.events.UserMetadata
import com.vitorpamplona.quartz.events.ZapSplitSetup
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.persistentSetOf
@@ -50,6 +51,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.math.BigDecimal import java.math.BigDecimal
import java.util.Locale import java.util.Locale
import kotlin.math.round
@Stable @Stable
class AccountViewModel(val account: Account) : ViewModel() { class AccountViewModel(val account: Account) : ViewModel() {
@@ -219,18 +221,120 @@ class AccountViewModel(val account: Account) : ViewModel() {
} }
} }
private suspend fun innerZap(note: Note, amount: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit, zapType: LnZapEvent.ZapType) { private suspend fun innerZap(note: Note, amountMilliSats: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit, zapType: LnZapEvent.ZapType) {
val lud16 = note.event?.zapAddress() ?: note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim() val zapSplitSetup = note.event?.zapSplitSetup()
val zapsToSend = if (!zapSplitSetup.isNullOrEmpty()) {
zapSplitSetup
} else {
val lud16 = note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim()
if (lud16.isNullOrBlank()) { if (lud16.isNullOrBlank()) {
onError(context.getString(R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats)) onError(context.getString(R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats))
return return
} }
listOf(ZapSplitSetup(lud16, null, weight = 1.0, true))
}
val totalWeight = zapsToSend.sumOf { it.weight }
val invoicesToPayOnIntent = mutableListOf<String>()
zapsToSend.forEachIndexed { index, value ->
val outerProgressMin = index / zapsToSend.size.toFloat()
val outerProgressMax = (index + 1) / zapsToSend.size.toFloat()
val zapValue =
round((amountMilliSats * value.weight / totalWeight) / 1000f).toLong() * 1000
if (value.isLnAddress) {
innerZap(
lud16 = value.lnAddressOrPubKeyHex,
note = note,
amount = zapValue,
pollOption = pollOption,
message = message,
context = context,
onError = onError,
onProgress = {
onProgress((it * (outerProgressMax - outerProgressMin)) + outerProgressMin)
},
zapType = zapType,
onPayInvoiceThroughIntent = {
invoicesToPayOnIntent.add(it)
}
)
} else {
val user = LocalCache.getUserIfExists(value.lnAddressOrPubKeyHex)
val lud16 = user?.info?.lnAddress()
if (lud16 != null) {
innerZap(
lud16 = lud16,
note = note,
amount = zapValue,
pollOption = pollOption,
message = message,
context = context,
onError = onError,
onProgress = {
onProgress((it * (outerProgressMax - outerProgressMin)) + outerProgressMin)
},
zapType = zapType,
overrideUser = user,
onPayInvoiceThroughIntent = {
invoicesToPayOnIntent.add(it)
}
)
} else {
onError(
context.getString(
R.string.user_x_does_not_have_a_lightning_address_setup_to_receive_sats,
user?.toBestDisplayName() ?: value.lnAddressOrPubKeyHex
)
)
}
}
}
if (invoicesToPayOnIntent.isNotEmpty()) {
payInvoices(bolt11s = invoicesToPayOnIntent, context = context)
}
// Awaits for the event to come back to LocalCache.
viewModelScope.launch(Dispatchers.IO) {
delay(5000)
onProgress(1f)
}
}
private suspend fun payInvoices(bolt11s: List<String>, context: Context) {
val uri = "lightning:" + bolt11s.joinToString("&")
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri))
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
ContextCompat.startActivity(context, intent, null)
}
private suspend fun innerZap(
lud16: String,
note: Note,
amount: Long,
pollOption: Int?,
message: String,
context: Context,
onError: (String) -> Unit,
onProgress: (percent: Float) -> Unit,
onPayInvoiceThroughIntent: (String) -> Unit,
zapType: LnZapEvent.ZapType,
overrideUser: User? = null
) {
var zapRequestJson = "" var zapRequestJson = ""
if (zapType != LnZapEvent.ZapType.NONZAP) { if (zapType != LnZapEvent.ZapType.NONZAP) {
val zapRequest = account.createZapRequestFor(note, pollOption, message, zapType) val zapRequest = account.createZapRequestFor(note, pollOption, message, zapType, overrideUser)
if (zapRequest != null) { if (zapRequest != null) {
zapRequestJson = zapRequest.toJson() zapRequestJson = zapRequest.toJson()
} }
@@ -263,19 +367,11 @@ class AccountViewModel(val account: Account) : ViewModel() {
} }
) )
onProgress(0.8f) onProgress(0.8f)
// Awaits for the event to come back to LocalCache.
viewModelScope.launch(Dispatchers.IO) {
delay(5000)
onProgress(0f)
}
} else { } else {
try { try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$it")) onPayInvoiceThroughIntent(it)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
ContextCompat.startActivity(context, intent, null)
} catch (e: Exception) { } catch (e: Exception) {
onError(context.getString(R.string.lightning_wallets_not_found)) onError(context.getString(R.string.lightning_wallets_not_found2))
} }
onProgress(0f) onProgress(0f)
} }
+12
View File
@@ -570,4 +570,16 @@
<string name="active_for_chats">Chats</string> <string name="active_for_chats">Chats</string>
<string name="active_for_global">Global</string> <string name="active_for_global">Global</string>
<string name="active_for_search">Search</string> <string name="active_for_search">Search</string>
<string name="zap_split_title">Split and Forward Zaps</string>
<string name="zap_split_explainer">Supporting clients will split and forward zaps to the users added here instead of yours</string>
<string name="zap_split_serarch_and_add_user">Search and Add User</string>
<string name="zap_split_serarch_and_add_user_placeholder">"@User"</string>
<string name="user_x_does_not_have_a_lightning_address_setup_to_receive_sats">User %1$s does not have a lightning address set up to receive sats</string>
<string name="zap_split_weight">Percentage</string>
<string name="zap_split_weight_placeholder">25</string>
<string name="splitting_zaps_with">Splitting zaps with</string>
<string name="forwarding_zaps_to">Forwarding zaps to</string>
<string name="lightning_wallets_not_found2">Lightning wallets not found</string>
</resources> </resources>
@@ -0,0 +1,89 @@
package com.vitorpamplona.amethyst
import com.vitorpamplona.amethyst.ui.components.Split
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class SplitterTest {
@Test
fun testSplit() {
val mySplit = Split<String>()
val vitor = mySplit.addItem("Vitor")
assertEquals(1f, mySplit.items[vitor].percentage, 0.01f)
assertTrue(mySplit.isEqualSplit())
val pablo = mySplit.addItem("Pablo")
assertEquals(0.5f, mySplit.items[pablo].percentage, 0.01f)
assertEquals(0.5f, mySplit.items[vitor].percentage, 0.01f)
assertTrue(mySplit.isEqualSplit())
val gigi = mySplit.addItem("Gigi")
assertEquals(0.33f, mySplit.items[vitor].percentage, 0.01f)
assertEquals(0.33f, mySplit.items[pablo].percentage, 0.01f)
assertEquals(0.33f, mySplit.items[gigi].percentage, 0.01f)
assertTrue(mySplit.isEqualSplit())
mySplit.updatePercentage(vitor, 0.5f)
assertEquals(0.5f, mySplit.items[vitor].percentage, 0.01f)
assertEquals(0.33f, mySplit.items[pablo].percentage, 0.01f)
assertEquals(0.16f, mySplit.items[gigi].percentage, 0.01f)
assertFalse(mySplit.isEqualSplit())
mySplit.updatePercentage(vitor, 0.95f)
assertEquals(0.95f, mySplit.items[vitor].percentage, 0.01f)
assertEquals(0.05f, mySplit.items[pablo].percentage, 0.01f)
assertEquals(0.0f, mySplit.items[gigi].percentage, 0.01f)
assertFalse(mySplit.isEqualSplit())
mySplit.updatePercentage(vitor, 0.15f)
assertEquals(0.15f, mySplit.items[vitor].percentage, 0.01f)
assertEquals(0.05f, mySplit.items[pablo].percentage, 0.01f)
assertEquals(0.80f, mySplit.items[gigi].percentage, 0.01f)
assertFalse(mySplit.isEqualSplit())
mySplit.updatePercentage(pablo, 0.95f)
assertEquals(0.05f, mySplit.items[vitor].percentage, 0.01f)
assertEquals(0.95f, mySplit.items[pablo].percentage, 0.01f)
assertEquals(0.00f, mySplit.items[gigi].percentage, 0.01f)
mySplit.updatePercentage(gigi, 1f)
assertEquals(0.00f, mySplit.items[vitor].percentage, 0.01f)
assertEquals(0.00f, mySplit.items[pablo].percentage, 0.01f)
assertEquals(1.00f, mySplit.items[gigi].percentage, 0.01f)
mySplit.updatePercentage(vitor, 0.5f)
assertEquals(0.50f, mySplit.items[vitor].percentage, 0.01f)
assertEquals(0.00f, mySplit.items[pablo].percentage, 0.01f)
assertEquals(0.50f, mySplit.items[gigi].percentage, 0.01f)
mySplit.updatePercentage(pablo, 0.3f)
assertEquals(0.50f, mySplit.items[vitor].percentage, 0.01f)
assertEquals(0.30f, mySplit.items[pablo].percentage, 0.01f)
assertEquals(0.20f, mySplit.items[gigi].percentage, 0.01f)
mySplit.updatePercentage(gigi, 1f)
assertEquals(0.00f, mySplit.items[vitor].percentage, 0.01f)
assertEquals(0.00f, mySplit.items[pablo].percentage, 0.01f)
assertEquals(1.00f, mySplit.items[gigi].percentage, 0.01f)
mySplit.updatePercentage(gigi, 0.5f)
assertEquals(0.00f, mySplit.items[vitor].percentage, 0.01f)
assertEquals(0.50f, mySplit.items[pablo].percentage, 0.01f)
assertEquals(0.50f, mySplit.items[gigi].percentage, 0.01f)
}
}
@@ -32,7 +32,7 @@ class ChannelMessageEvent(
channel: String, channel: String,
replyTos: List<String>? = null, replyTos: List<String>? = null,
mentions: List<String>? = null, mentions: List<String>? = null,
zapReceiver: String?, zapReceiver: List<ZapSplitSetup>? = null,
privateKey: ByteArray, privateKey: ByteArray,
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
markAsSensitive: Boolean, markAsSensitive: Boolean,
@@ -50,8 +50,8 @@ class ChannelMessageEvent(
mentions?.forEach { mentions?.forEach {
tags.add(listOf("p", it)) tags.add(listOf("p", it))
} }
zapReceiver?.let { zapReceiver?.forEach {
tags.add(listOf("zap", it)) tags.add(listOf("zap", it.lnAddressOrPubKeyHex, it.relay ?: "", it.weight.toString()))
} }
if (markAsSensitive) { if (markAsSensitive) {
tags.add(listOf("content-warning", "")) tags.add(listOf("content-warning", ""))
@@ -57,7 +57,7 @@ class ChatMessageEvent(
subject: String? = null, subject: String? = null,
replyTos: List<String>? = null, replyTos: List<String>? = null,
mentions: List<String>? = null, mentions: List<String>? = null,
zapReceiver: String? = null, zapReceiver: List<ZapSplitSetup>? = null,
markAsSensitive: Boolean = false, markAsSensitive: Boolean = false,
zapRaiserAmount: Long? = null, zapRaiserAmount: Long? = null,
geohash: String? = null, geohash: String? = null,
@@ -75,8 +75,8 @@ class ChatMessageEvent(
mentions?.forEach { mentions?.forEach {
tags.add(listOf("p", it, "", "mention")) tags.add(listOf("p", it, "", "mention"))
} }
zapReceiver?.let { zapReceiver?.forEach {
tags.add(listOf("zap", it)) tags.add(listOf("zap", it.lnAddressOrPubKeyHex, it.relay ?: "", it.weight.toString()))
} }
if (markAsSensitive) { if (markAsSensitive) {
tags.add(listOf("content-warning", "")) tags.add(listOf("content-warning", ""))
@@ -93,7 +93,21 @@ open class Event(
(it.size > 1 && it[0] == "zapraiser") (it.size > 1 && it[0] == "zapraiser")
}?.get(1)?.toLongOrNull() }?.get(1)?.toLongOrNull()
override fun zapAddress() = tags.firstOrNull { it.size > 1 && it[0] == "zap" }?.get(1) override fun hasZapSplitSetup() = tags.any { it.size > 1 && it[0] == "zap" }
override fun zapSplitSetup(): List<ZapSplitSetup> {
return tags.filter { it.size > 1 && it[0] == "zap" }.map {
val isLnAddress = it[0].contains("@") || it[0].startsWith("LNURL", true)
val weight = if (isLnAddress) 1.0 else (it.getOrNull(3)?.toDoubleOrNull() ?: 0.0)
ZapSplitSetup(
it[1],
it.getOrNull(2),
weight,
isLnAddress
)
}
}
override fun taggedAddresses() = tags.filter { it.size > 1 && it[0] == "a" }.mapNotNull { override fun taggedAddresses() = tags.filter { it.size > 1 && it[0] == "a" }.mapNotNull {
val aTagValue = it[1] val aTagValue = it[1]
@@ -421,3 +435,9 @@ fun String.bytesUsedInMemory(): Int {
return (8 * ((((this.length) * 2) + 45) / 8)) return (8 * ((((this.length) * 2) + 45) / 8))
} }
data class ZapSplitSetup(
val lnAddressOrPubKeyHex: String,
val relay: String?,
val weight: Double,
val isLnAddress: Boolean,
)
@@ -58,7 +58,7 @@ interface EventInterface {
fun getPoWRank(): Int fun getPoWRank(): Int
fun getGeoHash(): String? fun getGeoHash(): String?
fun zapAddress(): String? fun zapSplitSetup(): List<ZapSplitSetup>
fun isSensitive(): Boolean fun isSensitive(): Boolean
fun subject(): String? fun subject(): String?
fun zapraiserAmount(): Long? fun zapraiserAmount(): Long?
@@ -78,4 +78,5 @@ interface EventInterface {
fun taggedEmojis(): List<EmojiUrl> fun taggedEmojis(): List<EmojiUrl>
fun matchTag1With(text: String): Boolean fun matchTag1With(text: String): Boolean
fun isExpired(): Boolean fun isExpired(): Boolean
fun hasZapSplitSetup(): Boolean
} }
@@ -48,7 +48,7 @@ class LiveActivitiesChatMessageEvent(
activity: ATag, activity: ATag,
replyTos: List<String>? = null, replyTos: List<String>? = null,
mentions: List<String>? = null, mentions: List<String>? = null,
zapReceiver: String?, zapReceiver: List<ZapSplitSetup>? = null,
privateKey: ByteArray, privateKey: ByteArray,
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
markAsSensitive: Boolean, markAsSensitive: Boolean,
@@ -66,8 +66,8 @@ class LiveActivitiesChatMessageEvent(
mentions?.forEach { mentions?.forEach {
tags.add(listOf("p", it)) tags.add(listOf("p", it))
} }
zapReceiver?.let { zapReceiver?.forEach {
tags.add(listOf("zap", it)) tags.add(listOf("zap", it.lnAddressOrPubKeyHex, it.relay ?: "", it.weight.toString()))
} }
if (markAsSensitive) { if (markAsSensitive) {
tags.add(listOf("content-warning", "")) tags.add(listOf("content-warning", ""))
@@ -65,6 +65,7 @@ class LnZapRequestEvent(
pollOption: Int?, pollOption: Int?,
message: String, message: String,
zapType: LnZapEvent.ZapType, zapType: LnZapEvent.ZapType,
toUserPubHex: String?, // Overrides in case of Zap Splits
createdAt: Long = TimeUtils.now() createdAt: Long = TimeUtils.now()
): LnZapRequestEvent { ): LnZapRequestEvent {
var content = message var content = message
@@ -72,7 +73,7 @@ class LnZapRequestEvent(
var pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() var pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
var tags = listOf( var tags = listOf(
listOf("e", originalNote.id()), listOf("e", originalNote.id()),
listOf("p", originalNote.pubKey()), listOf("p", toUserPubHex ?: originalNote.pubKey()),
listOf("relays") + relays listOf("relays") + relays
) )
if (originalNote is AddressableEvent) { if (originalNote is AddressableEvent) {
@@ -12,7 +12,7 @@ class NIP24Factory {
subject: String? = null, subject: String? = null,
replyTos: List<String>? = null, replyTos: List<String>? = null,
mentions: List<String>? = null, mentions: List<String>? = null,
zapReceiver: String? = null, zapReceiver: List<ZapSplitSetup>? = null,
markAsSensitive: Boolean = false, markAsSensitive: Boolean = false,
zapRaiserAmount: Long? = null, zapRaiserAmount: Long? = null,
geohash: String? = null geohash: String? = null
@@ -52,7 +52,7 @@ class PollNoteEvent(
valueMinimum: Int?, valueMinimum: Int?,
consensusThreshold: Int?, consensusThreshold: Int?,
closedAt: Int?, closedAt: Int?,
zapReceiver: String?, zapReceiver: List<ZapSplitSetup>? = null,
markAsSensitive: Boolean, markAsSensitive: Boolean,
zapRaiserAmount: Long?, zapRaiserAmount: Long?,
geohash: String? = null geohash: String? = null
@@ -76,8 +76,8 @@ class PollNoteEvent(
tags.add(listOf(CONSENSUS_THRESHOLD, consensusThreshold.toString())) tags.add(listOf(CONSENSUS_THRESHOLD, consensusThreshold.toString()))
tags.add(listOf(CLOSED_AT, closedAt.toString())) tags.add(listOf(CLOSED_AT, closedAt.toString()))
if (zapReceiver != null) { zapReceiver?.forEach {
tags.add(listOf("zap", zapReceiver)) tags.add(listOf("zap", it.lnAddressOrPubKeyHex, it.relay ?: "", it.weight.toString()))
} }
if (markAsSensitive) { if (markAsSensitive) {
tags.add(listOf("content-warning", "")) tags.add(listOf("content-warning", ""))
@@ -85,7 +85,7 @@ class PrivateDmEvent(
msg: String, msg: String,
replyTos: List<String>? = null, replyTos: List<String>? = null,
mentions: List<String>? = null, mentions: List<String>? = null,
zapReceiver: String?, zapReceiver: List<ZapSplitSetup>? = null,
privateKey: ByteArray, privateKey: ByteArray,
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
publishedRecipientPubKey: ByteArray? = null, publishedRecipientPubKey: ByteArray? = null,
@@ -110,8 +110,8 @@ class PrivateDmEvent(
mentions?.forEach { mentions?.forEach {
tags.add(listOf("p", it)) tags.add(listOf("p", it))
} }
zapReceiver?.let { zapReceiver?.forEach {
tags.add(listOf("zap", it)) tags.add(listOf("zap", it.lnAddressOrPubKeyHex, it.relay ?: "", it.weight.toString()))
} }
if (markAsSensitive) { if (markAsSensitive) {
tags.add(listOf("content-warning", "")) tags.add(listOf("content-warning", ""))
@@ -30,7 +30,7 @@ class TextNoteEvent(
mentions: List<String>?, mentions: List<String>?,
addresses: List<ATag>?, addresses: List<ATag>?,
extraTags: List<String>?, extraTags: List<String>?,
zapReceiver: String?, zapReceiver: List<ZapSplitSetup>? = null,
markAsSensitive: Boolean, markAsSensitive: Boolean,
zapRaiserAmount: Long?, zapRaiserAmount: Long?,
replyingTo: String?, replyingTo: String?,
@@ -80,8 +80,8 @@ class TextNoteEvent(
extraTags?.forEach { extraTags?.forEach {
tags.add(listOf("t", it)) tags.add(listOf("t", it))
} }
zapReceiver?.let { zapReceiver?.forEach {
tags.add(listOf("zap", it)) tags.add(listOf("zap", it.lnAddressOrPubKeyHex, it.relay ?: "", it.weight.toString()))
} }
findURLs(msg).forEach { findURLs(msg).forEach {
tags.add(listOf("r", it)) tags.add(listOf("r", it))