Support to pledge additional bounty amounts.

This commit is contained in:
Vitor Pamplona
2023-03-23 12:24:46 -04:00
parent 7a1470ef7b
commit 1a66319a6d
7 changed files with 208 additions and 13 deletions
@@ -357,7 +357,7 @@ class Account(
}
}
fun sendPost(message: String, replyTo: List<Note>?, mentions: List<User>?) {
fun sendPost(message: String, replyTo: List<Note>?, mentions: List<User>?, tags: List<String>? = null) {
if (!isWriteable()) return
val repliesToHex = replyTo?.filter { it.address() == null }?.map { it.idHex }
@@ -369,8 +369,10 @@ class Account(
replyTos = repliesToHex,
mentions = mentionsHex,
addresses = addresses,
extraTags = tags,
privateKey = loggedIn.privKey!!
)
Client.send(signedEvent)
LocalCache.consume(signedEvent)
}
@@ -67,7 +67,7 @@ open class Note(val idHex: String) {
return channelHex?.let { LocalCache.checkGetOrCreateChannel(it) }
}
open fun address() = (event as? LongTextNoteEvent)?.address()
open fun address(): ATag? = null
open fun createdAt() = event?.createdAt()
@@ -235,6 +235,21 @@ open class Note(val idHex: String) {
}.sumOf { it }
}
fun hasPledgeBy(user: User): Boolean {
return replies
.filter { it.event?.isTaggedHash("bounty-added-reward") ?: false }
.any {
val pledgeValue = try {
BigDecimal(it.event?.content())
} catch (e: Exception) {
null
// do nothing if it can't convert to bigdecimal
}
pledgeValue != null && it.author != user
}
}
fun pledgedAmountByOthers(): BigDecimal {
return replies
.filter { it.event?.isTaggedHash("bounty-added-reward") ?: false }
@@ -18,7 +18,15 @@ class TextNoteEvent(
companion object {
const val kind = 1
fun create(msg: String, replyTos: List<String>?, mentions: List<String>?, addresses: List<ATag>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): TextNoteEvent {
fun create(
msg: String,
replyTos: List<String>?,
mentions: List<String>?,
addresses: List<ATag>?,
extraTags: List<String>?,
privateKey: ByteArray,
createdAt: Long = Date().time / 1000
): TextNoteEvent {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf<List<String>>()
replyTos?.forEach {
@@ -33,6 +41,9 @@ class TextNoteEvent(
findHashtags(msg).forEach {
tags.add(listOf("t", it))
}
extraTags?.forEach {
tags.add(listOf("t", it))
}
val id = generateId(pubKey, createdAt, kind, tags, msg)
val sig = Utils.sign(id, privateKey)
return TextNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey())
@@ -0,0 +1,144 @@
package com.vitorpamplona.amethyst.ui.note
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.actions.PostButton
class AddBountyAmountViewModel : ViewModel() {
private var account: Account? = null
private var bounty: Note? = null
var nextAmount by mutableStateOf(TextFieldValue(""))
fun load(account: Account, bounty: Note?) {
this.account = account
this.bounty = bounty
}
fun sendPost() {
val newValue = nextAmount.text.trim().toLongOrNull()
if (newValue != null) {
account?.sendPost(
newValue.toString(),
listOfNotNull(bounty),
listOfNotNull(bounty?.author),
listOf("bounty-added-reward")
)
nextAmount = TextFieldValue("")
}
}
fun cancel() {
nextAmount = TextFieldValue("")
}
fun hasChanged(): Boolean {
return nextAmount.text.trim().toLongOrNull() != null
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun AddBountyAmountDialog(bounty: Note, account: Account, onClose: () -> Unit) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val postViewModel: AddBountyAmountViewModel = viewModel()
LaunchedEffect(account) {
postViewModel.load(account, bounty)
}
Dialog(
onDismissRequest = { onClose() },
properties = DialogProperties(
dismissOnClickOutside = false,
usePlatformDefaultWidth = false
)
) {
Surface() {
Column(modifier = Modifier.padding(10.dp).width(IntrinsicSize.Min)) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
CloseButton(onCancel = {
postViewModel.cancel()
onClose()
})
PostButton(
onPost = {
postViewModel.sendPost()
onClose()
},
isActive = postViewModel.hasChanged()
)
}
Spacer(modifier = Modifier.height(10.dp))
Row(
modifier = Modifier.padding(vertical = 5.dp),
verticalAlignment = Alignment.CenterVertically
) {
OutlinedTextField(
label = { Text(text = stringResource(R.string.pledge_amount_in_sats)) },
value = postViewModel.nextAmount,
onValueChange = {
postViewModel.nextAmount = it
},
keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.None,
keyboardType = KeyboardType.Number
),
placeholder = {
Text(
text = "10000, 50000, 5000000",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
},
singleLine = true
)
}
}
}
}
}
@@ -310,7 +310,7 @@ fun NoteCompose(
val baseReward = noteEvent.getReward()
if (baseReward != null) {
DisplayReward(baseReward, baseNote, navController)
DisplayReward(baseReward, baseNote, account, navController)
}
}
}
@@ -566,10 +566,16 @@ fun DisplayUncitedHashtags(
fun DisplayReward(
baseReward: BigDecimal,
baseNote: Note,
account: Account,
navController: NavController
) {
var popupExpanded by remember { mutableStateOf(false) }
Column() {
Row(verticalAlignment = Alignment.CenterVertically) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.clickable { popupExpanded = true }
) {
ClickableText(
text = AnnotatedString("#bounty"),
onClick = { navController.navigate("Hashtag/bounty") },
@@ -580,16 +586,25 @@ fun DisplayReward(
)
)
Icon(
imageVector = Icons.Default.Bolt,
contentDescription = stringResource(R.string.zaps),
modifier = Modifier.size(20.dp),
tint = BitcoinOrange
)
val repliesState by baseNote.live().replies.observeAsState()
val replyNote = repliesState?.note
if (replyNote?.hasPledgeBy(account.userProfile()) == true) {
Icon(
imageVector = Icons.Default.Bolt,
contentDescription = stringResource(R.string.zaps),
modifier = Modifier.size(20.dp),
tint = BitcoinOrange
)
} else {
Icon(
imageVector = Icons.Default.Bolt,
contentDescription = stringResource(R.string.zaps),
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
}
var rewardAmount by remember {
mutableStateOf<BigDecimal?>(
baseReward
@@ -609,6 +624,12 @@ fun DisplayReward(
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
}
if (popupExpanded) {
AddBountyAmountDialog(baseNote, account) {
popupExpanded = false
}
}
}
}
@@ -281,7 +281,7 @@ fun NoteMaster(
val baseReward = noteEvent.getReward()
if (baseReward != null) {
DisplayReward(baseReward, baseNote, navController)
DisplayReward(baseReward, baseNote, account, navController)
}
}
}
+2
View File
@@ -260,4 +260,6 @@
<string name="wallet_connect_service_explainer">Uses your private key to pay zaps without leaving the app. Anyone with access to your Nostr private key will be able to spend your wallet\'s balance. Only keep funds you are ok to lose and use a private relay if possible. The relay operator can see your payments metadata.</string>
<string name="wallet_connect_service_pubkey">Wallet Connect Pubkey</string>
<string name="wallet_connect_service_relay">Wallet Connect Relay</string>
<string name="pledge_amount_in_sats">Pledge Amount in Sats</string>
</resources>