kopia lustrzana https://github.com/vitorpamplona/amethyst
282 wiersze
10 KiB
Kotlin
282 wiersze
10 KiB
Kotlin
/**
|
|
* Copyright (c) 2024 Vitor Pamplona
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
* this software and associated documentation files (the "Software"), to deal in
|
|
* the Software without restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
|
* subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in all
|
|
* copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
*/
|
|
package com.vitorpamplona.amethyst.ui.note
|
|
|
|
import androidx.compose.runtime.MutableState
|
|
import androidx.compose.runtime.Stable
|
|
import androidx.compose.runtime.mutableStateOf
|
|
import androidx.lifecycle.ViewModel
|
|
import androidx.lifecycle.viewModelScope
|
|
import com.vitorpamplona.amethyst.model.Account
|
|
import com.vitorpamplona.amethyst.model.Note
|
|
import com.vitorpamplona.amethyst.model.User
|
|
import com.vitorpamplona.quartz.events.CLOSED_AT
|
|
import com.vitorpamplona.quartz.events.CONSENSUS_THRESHOLD
|
|
import com.vitorpamplona.quartz.events.LnZapEvent
|
|
import com.vitorpamplona.quartz.events.LnZapRequestEvent
|
|
import com.vitorpamplona.quartz.events.PollNoteEvent
|
|
import com.vitorpamplona.quartz.events.VALUE_MAXIMUM
|
|
import com.vitorpamplona.quartz.events.VALUE_MINIMUM
|
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.launch
|
|
import java.math.BigDecimal
|
|
import java.math.RoundingMode
|
|
|
|
@Stable
|
|
data class PollOption(
|
|
val option: Int,
|
|
val descriptor: String,
|
|
var zappedValue: MutableState<BigDecimal> = mutableStateOf(BigDecimal.ZERO),
|
|
var tally: MutableState<BigDecimal> = mutableStateOf(BigDecimal.ZERO),
|
|
var consensusThreadhold: MutableState<Boolean> = mutableStateOf(false),
|
|
var zappedByLoggedIn: MutableState<Boolean> = mutableStateOf(false),
|
|
)
|
|
|
|
@Stable
|
|
class PollNoteViewModel : ViewModel() {
|
|
private var account: Account? = null
|
|
private var pollNote: Note? = null
|
|
|
|
private var pollEvent: PollNoteEvent? = null
|
|
private var pollOptions: Map<Int, String>? = null
|
|
private var valueMaximum: Long? = null
|
|
private var valueMinimum: Long? = null
|
|
private var valueMaximumBD: BigDecimal? = null
|
|
private var valueMinimumBD: BigDecimal? = null
|
|
|
|
private var closedAt: Long? = null
|
|
private var consensusThreshold: BigDecimal? = null
|
|
|
|
private var totalZapped: BigDecimal = BigDecimal.ZERO
|
|
private var wasZappedByLoggedInAccount: Boolean = false
|
|
|
|
var canZap = mutableStateOf(false)
|
|
var tallies: List<PollOption> = emptyList()
|
|
|
|
fun load(
|
|
acc: Account,
|
|
note: Note?,
|
|
) {
|
|
if (acc != account || pollNote != note) {
|
|
account = acc
|
|
pollNote = note
|
|
pollEvent = pollNote?.event as PollNoteEvent
|
|
pollOptions = pollEvent?.pollOptions()
|
|
valueMaximum = pollEvent?.getTagLong(VALUE_MAXIMUM)
|
|
valueMinimum = pollEvent?.getTagLong(VALUE_MINIMUM)
|
|
valueMinimumBD = valueMinimum?.let { BigDecimal(it) }
|
|
valueMaximumBD = valueMaximum?.let { BigDecimal(it) }
|
|
consensusThreshold =
|
|
pollEvent?.getTagLong(CONSENSUS_THRESHOLD)?.toFloat()?.div(100)?.toBigDecimal()
|
|
closedAt = pollEvent?.getTagLong(CLOSED_AT)
|
|
|
|
totalZapped = BigDecimal.ZERO
|
|
wasZappedByLoggedInAccount = false
|
|
|
|
canZap.value = checkIfCanZap()
|
|
|
|
tallies = pollOptions?.keys?.map { option ->
|
|
PollOption(
|
|
option,
|
|
pollOptions?.get(option) ?: "",
|
|
)
|
|
} ?: emptyList()
|
|
}
|
|
}
|
|
|
|
fun refreshTallies() {
|
|
viewModelScope.launch(Dispatchers.Default) {
|
|
totalZapped = totalZapped()
|
|
wasZappedByLoggedInAccount = false
|
|
account?.calculateIfNoteWasZappedByAccount(pollNote) {
|
|
wasZappedByLoggedInAccount = true
|
|
canZap.value = checkIfCanZap()
|
|
}
|
|
|
|
tallies.forEach {
|
|
val zappedValue = zappedPollOptionAmount(it.option)
|
|
val tallyValue =
|
|
if (totalZapped > BigDecimal.ZERO) {
|
|
zappedValue.divide(totalZapped, 2, RoundingMode.HALF_UP)
|
|
} else {
|
|
BigDecimal.ZERO
|
|
}
|
|
|
|
it.zappedValue.value = zappedValue
|
|
it.tally.value = tallyValue
|
|
it.consensusThreadhold.value = consensusThreshold != null && tallyValue >= consensusThreshold!!
|
|
it.zappedByLoggedIn.value = account?.userProfile()?.let { it1 -> cachedIsPollOptionZappedBy(it.option, it1) } ?: false
|
|
}
|
|
}
|
|
}
|
|
|
|
fun checkIfCanZap(): Boolean {
|
|
val account = account ?: return false
|
|
val note = pollNote ?: return false
|
|
return account.userProfile() != note.author && !wasZappedByLoggedInAccount
|
|
}
|
|
|
|
fun isVoteAmountAtomic() = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum
|
|
|
|
fun isPollClosed(): Boolean =
|
|
closedAt?.let { // allow 2 minute leeway for zap to propagate
|
|
pollNote?.createdAt()?.plus(it * (86400 + 120))!! < TimeUtils.now()
|
|
} == true
|
|
|
|
fun voteAmountPlaceHolderText(sats: String): String =
|
|
if (valueMinimum == null && valueMaximum == null) {
|
|
sats
|
|
} else if (valueMinimum == null) {
|
|
"1—$valueMaximum $sats"
|
|
} else if (valueMaximum == null) {
|
|
">$valueMinimum $sats"
|
|
} else {
|
|
"$valueMinimum—$valueMaximum $sats"
|
|
}
|
|
|
|
fun inputVoteAmountLong(textAmount: String) =
|
|
if (textAmount.isEmpty()) {
|
|
null
|
|
} else {
|
|
try {
|
|
textAmount.toLong()
|
|
} catch (e: Exception) {
|
|
null
|
|
}
|
|
}
|
|
|
|
fun isValidInputVoteAmount(amount: BigDecimal?): Boolean {
|
|
if (amount == null) {
|
|
return false
|
|
} else if (valueMinimum == null && valueMaximum == null) {
|
|
if (amount > BigDecimal.ZERO) {
|
|
return true
|
|
}
|
|
} else if (valueMinimum == null) {
|
|
if (amount > BigDecimal.ZERO && amount <= valueMaximumBD!!) {
|
|
return true
|
|
}
|
|
} else if (valueMaximum == null) {
|
|
if (amount >= valueMinimumBD!!) {
|
|
return true
|
|
}
|
|
} else {
|
|
if ((valueMinimumBD!! <= amount) && (amount <= valueMaximumBD!!)) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
fun isValidInputVoteAmount(amount: Long?): Boolean {
|
|
if (amount == null) {
|
|
return false
|
|
} else if (valueMinimum == null && valueMaximum == null) {
|
|
if (amount > 0) {
|
|
return true
|
|
}
|
|
} else if (valueMinimum == null) {
|
|
if (amount > 0 && amount <= valueMaximum!!) {
|
|
return true
|
|
}
|
|
} else if (valueMaximum == null) {
|
|
if (amount >= valueMinimum!!) {
|
|
return true
|
|
}
|
|
} else {
|
|
if ((valueMinimum!! <= amount) && (amount <= valueMaximum!!)) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
suspend fun isPollOptionZappedBy(
|
|
option: Int,
|
|
user: User,
|
|
onWasZappedByAuthor: () -> Unit,
|
|
) {
|
|
pollNote?.isZappedBy(option, user, account!!, onWasZappedByAuthor)
|
|
}
|
|
|
|
fun cachedIsPollOptionZappedBy(
|
|
option: Int,
|
|
user: User,
|
|
): Boolean {
|
|
return pollNote!!.zaps.any {
|
|
val zapEvent = it.value?.event as? LnZapEvent
|
|
val privateZapAuthor = (it.key.event as? LnZapRequestEvent)?.cachedPrivateZap()
|
|
zapEvent?.zappedPollOption() == option &&
|
|
(it.key.author?.pubkeyHex == user.pubkeyHex || privateZapAuthor?.pubKey == user.pubkeyHex)
|
|
}
|
|
}
|
|
|
|
private fun zappedPollOptionAmount(option: Int): BigDecimal {
|
|
return pollNote?.zaps?.values?.sumOf {
|
|
val event = it?.event as? LnZapEvent
|
|
val zapAmount = event?.amount ?: BigDecimal.ZERO
|
|
val isValidAmount = isValidInputVoteAmount(event?.amount)
|
|
|
|
if (isValidAmount && event?.zappedPollOption() == option) {
|
|
zapAmount
|
|
} else {
|
|
BigDecimal.ZERO
|
|
}
|
|
}
|
|
?: BigDecimal.ZERO
|
|
}
|
|
|
|
private fun totalZapped(): BigDecimal {
|
|
return pollNote?.zaps?.values?.sumOf {
|
|
val zapEvent = (it?.event as? LnZapEvent)
|
|
val zapAmount = zapEvent?.amount ?: BigDecimal.ZERO
|
|
val isValidAmount = isValidInputVoteAmount(zapEvent?.amount)
|
|
|
|
if (isValidAmount && zapEvent?.zappedPollOption() != null) {
|
|
zapAmount
|
|
} else {
|
|
BigDecimal.ZERO
|
|
}
|
|
}
|
|
?: BigDecimal.ZERO
|
|
}
|
|
|
|
fun createZapOptionsThatMatchThePollingParameters(): List<Long> {
|
|
val options =
|
|
account?.zapAmountChoices?.filter { isValidInputVoteAmount(it) }?.toMutableList()
|
|
?: mutableListOf()
|
|
if (options.isEmpty()) {
|
|
valueMinimum?.let { minimum ->
|
|
valueMaximum?.let { maximum ->
|
|
if (minimum != maximum) {
|
|
options.add(((minimum + maximum) / 2).toLong())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
valueMinimum?.let { options.add(it) }
|
|
valueMaximum?.let { options.add(it) }
|
|
|
|
return options.toSet().sorted()
|
|
}
|
|
}
|