diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 8437d532b..32cc0563f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -237,6 +237,21 @@ open class Note(val idHex: String) { }.sumOf { it } } + fun zappedPollOptionAmount(option: Int): BigDecimal { + return zaps.mapNotNull { it.value?.event } + .filterIsInstance() + .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 { val dayAgo = Date().time / 1000 - 24 * 60 * 60 return reports.isNotEmpty() || diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt index a60118eaf..dd3a9bcf9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt @@ -19,9 +19,16 @@ class LnZapEvent( .filter { it.firstOrNull() == "e" } .mapNotNull { it.getOrNull(1) } +/* // TODO add poll_option tag to LnZapEvent override fun zappedPollOption(): Int? = tags .filter { it.firstOrNull() == "poll_option" } .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 .filter { it.firstOrNull() == "p" } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollVoteValueRange.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollVoteValueRange.kt index 3c4ec0908..63a075759 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollVoteValueRange.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollVoteValueRange.kt @@ -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.Row @@ -21,7 +21,6 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.actions.NewPollViewModel @Composable fun NewPollVoteValueRange(pollViewModel: NewPollViewModel) { @@ -93,7 +92,7 @@ fun NewPollVoteValueRange(pollViewModel: NewPollViewModel) { }, placeholder = { Text( - text = stringResource(R.string.poll_zap_amount), + text = stringResource(R.string.sats), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) } @@ -112,7 +111,7 @@ fun NewPollVoteValueRange(pollViewModel: NewPollViewModel) { }, placeholder = { Text( - text = stringResource(R.string.poll_zap_amount), + text = stringResource(R.string.sats), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt index 6211017ec..c1611a41f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt @@ -1,13 +1,11 @@ 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.combinedClickable +import androidx.compose.foundation.* import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.* import androidx.compose.material.icons.Icons 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.runtime.* 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.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.input.KeyboardType 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.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties import androidx.navigation.NavController import com.vitorpamplona.amethyst.R 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.theme.BitcoinOrange import kotlinx.coroutines.launch +import java.util.* @Composable fun PollNote( @@ -44,6 +44,7 @@ fun PollNote( navController: NavController ) { val pollEvent = note.event as PollNoteEvent + val consensusThreshold = pollEvent.consensusThreshold() pollEvent.pollOptions().forEach { poll_op -> Row(Modifier.fillMaxWidth()) { @@ -86,6 +87,15 @@ fun ZapVote( val context = LocalContext.current 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( modifier = Modifier .then(Modifier.size(20.dp)) @@ -104,10 +114,20 @@ fun ZapVote( ) .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( baseNote, - account.zapAmountChoices.first() * 1000, + valueMaximum!!.toLong() * 1000, pollOption, "", context @@ -118,7 +138,7 @@ fun ZapVote( .show() } } - } else if (account.zapAmountChoices.size > 1) { + } else { wantsToZap = true } }, @@ -130,6 +150,8 @@ fun ZapVote( baseNote, accountViewModel, pollOption, + valueMinimum, + valueMaximum, onDismiss = { wantsToZap = false }, @@ -141,7 +163,7 @@ fun ZapVote( ) } - if (zappedNote?.isZappedBy(account.userProfile()) == true) { + if (zappedNote?.isPollOptionZapped(pollOption) == true) { Icon( imageVector = Icons.Default.Bolt, contentDescription = stringResource(R.string.zaps), @@ -159,7 +181,7 @@ fun ZapVote( } Text( - showAmount(zappedNote?.zappedAmount()), + showAmount(zappedNote?.zappedPollOptionAmount(pollOption)), fontSize = 14.sp, color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), modifier = modifier @@ -172,45 +194,135 @@ fun ZapVoteAmountChoicePopup( baseNote: Note, accountViewModel: AccountViewModel, pollOption: Int, + valueMinimum: Int?, + valueMaximum: Int?, onDismiss: () -> Unit, onError: (text: String) -> Unit ) { val context = LocalContext.current - val accountState by accountViewModel.accountLiveData.observeAsState() - val account = accountState?.account ?: return + var textAmount by rememberSaveable { mutableStateOf("") } - Popup( - alignment = Alignment.BottomCenter, - offset = IntOffset(0, -50), - onDismissRequest = { onDismiss() } + val placeHolderText = if (valueMinimum == null && valueMaximum == null) { + stringResource(R.string.sats) + } else if (valueMinimum == null) { + "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) { - account.zapAmountChoices.forEach { amountInSats -> - Button( - modifier = Modifier.padding(horizontal = 3.dp), - onClick = { - accountViewModel.zap(baseNote, amountInSats * 1000, pollOption, "", context, onError) - onDismiss() + Surface { + 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) + ) }, - shape = RoundedCornerShape(20.dp), - colors = ButtonDefaults - .buttonColors( - backgroundColor = MaterialTheme.colors.primary + placeholder = { + Text( + text = placeHolderText, + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) - ) { - 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 = {} + } + ) + + if (amount != null && isValidAmount) { + Button( + modifier = Modifier.padding(horizontal = 3.dp), + onClick = { + accountViewModel.zap( + baseNote, + amount * 1000, + pollOption, + "", + context, + onError + ) + onDismiss() + }, + shape = RoundedCornerShape(20.dp), + colors = ButtonDefaults + .buttonColors( + backgroundColor = MaterialTheme.colors.primary + ) + ) { + Text( + "⚡ ${showAmount(amount.toBigDecimal().setScale(1))}", + color = Color.White, + textAlign = TextAlign.Center, + modifier = Modifier.combinedClickable( + onClick = { + accountViewModel.zap( + baseNote, + amount * 1000, + pollOption, + "", + context, + onError + ) + onDismiss() + }, + onLongClick = {} + ) ) - ) + } } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2a5c1bdfd..0fa1498cc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -231,8 +231,9 @@ Zap maximum Consensus (0–100)% - sats Close after days + Poll is closed to new votes + Zap amount