add zap vote amount dialog,

show vote option tallies,
format zapped vote button icons
This commit is contained in:
toadlyBroodle
2023-03-24 18:10:02 +09:00
parent 171a7841b3
commit dbf0256b1c
5 changed files with 180 additions and 46 deletions

View File

@@ -237,6 +237,21 @@ open class Note(val idHex: String) {
}.sumOf { it } }.sumOf { it }
} }
fun zappedPollOptionAmount(option: Int): BigDecimal {
return zaps.mapNotNull { it.value?.event }
.filterIsInstance<LnZapEvent>()
.mapNotNull {
val zappedOption = it.zappedPollOption()
if (zappedOption == option) {
it.amount
} else { null }
}.sumOf { it }
}
fun isPollOptionZapped(option: Int): Boolean {
return zappedPollOptionAmount(option).toInt() > 0
}
fun hasAnyReports(): Boolean { fun hasAnyReports(): Boolean {
val dayAgo = Date().time / 1000 - 24 * 60 * 60 val dayAgo = Date().time / 1000 - 24 * 60 * 60
return reports.isNotEmpty() || return reports.isNotEmpty() ||

View File

@@ -19,9 +19,16 @@ class LnZapEvent(
.filter { it.firstOrNull() == "e" } .filter { it.firstOrNull() == "e" }
.mapNotNull { it.getOrNull(1) } .mapNotNull { it.getOrNull(1) }
/* // TODO add poll_option tag to LnZapEvent
override fun zappedPollOption(): Int? = tags override fun zappedPollOption(): Int? = tags
.filter { it.firstOrNull() == "poll_option" } .filter { it.firstOrNull() == "poll_option" }
.getOrNull(1)?.getOrNull(1)?.toInt() .getOrNull(1)?.getOrNull(1)?.toInt()
*/
// TODO replace this hacky way to get poll option with above function
override fun zappedPollOption(): Int? = description()
?.substringAfter("poll_option\",\"")
?.substringBefore("\"")
?.toInt()
override fun zappedAuthor() = tags override fun zappedAuthor() = tags
.filter { it.firstOrNull() == "p" } .filter { it.firstOrNull() == "p" }

View File

@@ -1,4 +1,4 @@
package com.vitorpamplona.amethyst.ui.components package com.vitorpamplona.amethyst.ui.actions
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@@ -21,7 +21,6 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.NewPollViewModel
@Composable @Composable
fun NewPollVoteValueRange(pollViewModel: NewPollViewModel) { fun NewPollVoteValueRange(pollViewModel: NewPollViewModel) {
@@ -93,7 +92,7 @@ fun NewPollVoteValueRange(pollViewModel: NewPollViewModel) {
}, },
placeholder = { placeholder = {
Text( Text(
text = stringResource(R.string.poll_zap_amount), text = stringResource(R.string.sats),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
) )
} }
@@ -112,7 +111,7 @@ fun NewPollVoteValueRange(pollViewModel: NewPollViewModel) {
}, },
placeholder = { placeholder = {
Text( Text(
text = stringResource(R.string.poll_zap_amount), text = stringResource(R.string.sats),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
) )
} }

View File

@@ -1,13 +1,11 @@
package com.vitorpamplona.amethyst.ui.note package com.vitorpamplona.amethyst.ui.note
import android.widget.Toast import android.widget.Toast
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.*
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.border
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bolt import androidx.compose.material.icons.filled.Bolt
@@ -15,17 +13,18 @@ import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.material.ripple.rememberRipple import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment import androidx.compose.runtime.saveable.rememberSaveable
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.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign 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.unit.sp
import androidx.compose.ui.window.Popup import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.navigation.NavController import androidx.navigation.NavController
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
@@ -34,6 +33,7 @@ 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 com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.*
@Composable @Composable
fun PollNote( fun PollNote(
@@ -44,6 +44,7 @@ fun PollNote(
navController: NavController navController: NavController
) { ) {
val pollEvent = note.event as PollNoteEvent val pollEvent = note.event as PollNoteEvent
val consensusThreshold = pollEvent.consensusThreshold()
pollEvent.pollOptions().forEach { poll_op -> pollEvent.pollOptions().forEach { poll_op ->
Row(Modifier.fillMaxWidth()) { Row(Modifier.fillMaxWidth()) {
@@ -86,6 +87,15 @@ fun ZapVote(
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val pollEvent = baseNote.event as PollNoteEvent
val valueMaximum = pollEvent.valueMaximum()
val valueMinimum = pollEvent.valueMinimum()
val isPollClosed: Boolean = pollEvent.closedAt()?.let { // allow 2 minute leeway for zap to propagate
baseNote.createdAt()?.plus(it * (86400 + 120))!! > Date().time / 1000
} == true
val isVoteAmountAtomic = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum
Row( Row(
modifier = Modifier modifier = Modifier
.then(Modifier.size(20.dp)) .then(Modifier.size(20.dp))
@@ -104,10 +114,20 @@ fun ZapVote(
) )
.show() .show()
} }
} else if (account.zapAmountChoices.size == 1) { } else if (isPollClosed) {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.poll_is_closed),
Toast.LENGTH_SHORT
)
.show()
}
} else if (isVoteAmountAtomic) {
accountViewModel.zap( accountViewModel.zap(
baseNote, baseNote,
account.zapAmountChoices.first() * 1000, valueMaximum!!.toLong() * 1000,
pollOption, pollOption,
"", "",
context context
@@ -118,7 +138,7 @@ fun ZapVote(
.show() .show()
} }
} }
} else if (account.zapAmountChoices.size > 1) { } else {
wantsToZap = true wantsToZap = true
} }
}, },
@@ -130,6 +150,8 @@ fun ZapVote(
baseNote, baseNote,
accountViewModel, accountViewModel,
pollOption, pollOption,
valueMinimum,
valueMaximum,
onDismiss = { onDismiss = {
wantsToZap = false wantsToZap = false
}, },
@@ -141,7 +163,7 @@ fun ZapVote(
) )
} }
if (zappedNote?.isZappedBy(account.userProfile()) == true) { if (zappedNote?.isPollOptionZapped(pollOption) == true) {
Icon( Icon(
imageVector = Icons.Default.Bolt, imageVector = Icons.Default.Bolt,
contentDescription = stringResource(R.string.zaps), contentDescription = stringResource(R.string.zaps),
@@ -159,7 +181,7 @@ fun ZapVote(
} }
Text( Text(
showAmount(zappedNote?.zappedAmount()), showAmount(zappedNote?.zappedPollOptionAmount(pollOption)),
fontSize = 14.sp, fontSize = 14.sp,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
modifier = modifier modifier = modifier
@@ -172,25 +194,107 @@ fun ZapVoteAmountChoicePopup(
baseNote: Note, baseNote: Note,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
pollOption: Int, pollOption: Int,
valueMinimum: Int?,
valueMaximum: Int?,
onDismiss: () -> Unit, onDismiss: () -> Unit,
onError: (text: String) -> Unit onError: (text: String) -> Unit
) { ) {
val context = LocalContext.current val context = LocalContext.current
val accountState by accountViewModel.accountLiveData.observeAsState() var textAmount by rememberSaveable { mutableStateOf("") }
val account = accountState?.account ?: return
Popup( val placeHolderText = if (valueMinimum == null && valueMaximum == null) {
alignment = Alignment.BottomCenter, stringResource(R.string.sats)
offset = IntOffset(0, -50), } else if (valueMinimum == null) {
onDismissRequest = { onDismiss() } "1—$valueMaximum " + stringResource(R.string.sats)
} else if (valueMaximum == null) {
">$valueMinimum " + stringResource(R.string.sats)
} else {
"$valueMinimum$valueMaximum " + stringResource(R.string.sats)
}
val amount = if (textAmount.isEmpty()) { null } else {
try {
textAmount.toLong()
} catch (e: Exception) { null }
}
var isValidAmount = false
if (amount == null) {
isValidAmount = false
} else if (valueMinimum == null && valueMaximum == null) {
if (amount > 0) {
isValidAmount = true
}
} else if (valueMinimum == null) {
if (amount > 0 && amount <= valueMaximum!!) {
isValidAmount = true
}
} else if (valueMaximum == null) {
if (amount >= valueMinimum) {
isValidAmount = true
}
} else {
if ((valueMinimum <= amount) && (amount <= valueMaximum)) {
isValidAmount = true
}
}
val colorInValid = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = MaterialTheme.colors.error,
unfocusedBorderColor = Color.Red
)
val colorValid = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = MaterialTheme.colors.primary,
unfocusedBorderColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
Dialog(
onDismissRequest = { onDismiss() },
properties = DialogProperties(
dismissOnClickOutside = true,
usePlatformDefaultWidth = false
)
) { ) {
FlowRow(horizontalArrangement = Arrangement.Center) { Surface {
account.zapAmountChoices.forEach { amountInSats -> Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.background(MaterialTheme.colors.primary)
.padding(10.dp)
) {
OutlinedTextField(
value = textAmount,
onValueChange = { textAmount = it },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.width(150.dp),
colors = if (isValidAmount) colorValid else colorInValid,
label = {
Text(
text = stringResource(R.string.poll_zap_amount),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
},
placeholder = {
Text(
text = placeHolderText,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
}
)
if (amount != null && isValidAmount) {
Button( Button(
modifier = Modifier.padding(horizontal = 3.dp), modifier = Modifier.padding(horizontal = 3.dp),
onClick = { onClick = {
accountViewModel.zap(baseNote, amountInSats * 1000, pollOption, "", context, onError) accountViewModel.zap(
baseNote,
amount * 1000,
pollOption,
"",
context,
onError
)
onDismiss() onDismiss()
}, },
shape = RoundedCornerShape(20.dp), shape = RoundedCornerShape(20.dp),
@@ -200,12 +304,19 @@ fun ZapVoteAmountChoicePopup(
) )
) { ) {
Text( Text(
"${showAmount(amountInSats.toBigDecimal().setScale(1))}", "${showAmount(amount.toBigDecimal().setScale(1))}",
color = Color.White, color = Color.White,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.combinedClickable( modifier = Modifier.combinedClickable(
onClick = { onClick = {
accountViewModel.zap(baseNote, amountInSats * 1000, pollOption, "", context, onError) accountViewModel.zap(
baseNote,
amount * 1000,
pollOption,
"",
context,
onError
)
onDismiss() onDismiss()
}, },
onLongClick = {} onLongClick = {}
@@ -215,4 +326,5 @@ fun ZapVoteAmountChoicePopup(
} }
} }
} }
}
} }

View File

@@ -231,8 +231,9 @@
<string name="poll_zap_value_max">Zap maximum</string> <string name="poll_zap_value_max">Zap maximum</string>
<string name="poll_consensus_threshold">Consensus</string> <string name="poll_consensus_threshold">Consensus</string>
<string name="poll_consensus_threshold_percent">(0100)%</string> <string name="poll_consensus_threshold_percent">(0100)%</string>
<string name="poll_zap_amount">sats</string>
<string name="poll_closing_time">Close after</string> <string name="poll_closing_time">Close after</string>
<string name="poll_closing_time_days">days</string> <string name="poll_closing_time_days">days</string>
<string name="poll_is_closed">Poll is closed to new votes</string>
<string name="poll_zap_amount">Zap amount</string>
</resources> </resources>