This commit is contained in:
Vitor Pamplona
2023-05-14 17:03:46 -04:00
parent 6842d12b39
commit f84501e40f
4 changed files with 104 additions and 87 deletions
@@ -411,9 +411,9 @@ class NoteLiveSet(u: Note) {
class NoteLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) { class NoteLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
// Refreshes observers in batches. // Refreshes observers in batches.
private val bundler = BundledUpdate(300, Dispatchers.IO) { private val bundler = BundledUpdate(300, Dispatchers.IO) {
if (hasActiveObservers()) { // if (hasObservers()) {
postValue(NoteState(note)) postValue(NoteState(note))
} // }
} }
fun invalidateData() { fun invalidateData() {
@@ -71,9 +71,9 @@ class BundledInsert<T>(
delay(delay) delay(delay)
mySet.clear() val mySet2 = mutableSetOf<T>()
queue.drainTo(mySet) queue.drainTo(mySet2)
onUpdate(mySet) onUpdate(mySet2)
} finally { } finally {
withContext(NonCancellable) { withContext(NonCancellable) {
onlyOneInBlock.set(false) onlyOneInBlock.set(false)
@@ -28,6 +28,7 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.Popup import androidx.compose.ui.window.Popup
import androidx.lifecycle.viewmodel.compose.viewModel
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
@@ -37,7 +38,6 @@ import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.math.BigDecimal
import java.util.* import java.util.*
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -49,19 +49,45 @@ fun PollNote(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
navController: NavController navController: NavController
) { ) {
val zapsState by baseNote.live().zaps.observeAsState()
val zappedNote = zapsState?.note ?: return
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return val account = remember(accountState) { accountState?.account } ?: return
val pollViewModel = PollNoteViewModel() val pollViewModel: PollNoteViewModel = viewModel()
pollViewModel.load(account, zappedNote)
pollViewModel.pollEvent?.pollOptions()?.forEach { poll_op -> LaunchedEffect(key1 = baseNote) {
pollViewModel.load(account, baseNote)
}
PollNote(
baseNote = baseNote,
pollViewModel = pollViewModel,
canPreview = canPreview,
backgroundColor = backgroundColor,
accountViewModel = accountViewModel,
navController = navController
)
}
@Composable
fun PollNote(
baseNote: Note,
pollViewModel: PollNoteViewModel,
canPreview: Boolean,
backgroundColor: Color,
accountViewModel: AccountViewModel,
navController: NavController
) {
val zapsState by baseNote.live().zaps.observeAsState()
LaunchedEffect(key1 = zapsState) {
withContext(Dispatchers.IO) {
pollViewModel.refreshTallies()
}
}
pollViewModel.tallies.forEach { poll_op ->
OptionNote( OptionNote(
poll_op.key, poll_op,
poll_op.value,
pollViewModel, pollViewModel,
baseNote, baseNote,
accountViewModel, accountViewModel,
@@ -74,8 +100,7 @@ fun PollNote(
@Composable @Composable
private fun OptionNote( private fun OptionNote(
optionNumber: Int, poolOption: PollOption,
optionText: String,
pollViewModel: PollNoteViewModel, pollViewModel: PollNoteViewModel,
baseNote: Note, baseNote: Note,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
@@ -88,32 +113,17 @@ private fun OptionNote(
modifier = Modifier.padding(vertical = 3.dp) modifier = Modifier.padding(vertical = 3.dp)
) { ) {
if (!pollViewModel.canZap()) { if (!pollViewModel.canZap()) {
val defaultColor = MaterialTheme.colors.primary.copy(alpha = 0.32f) val color = if (poolOption.consensusThreadhold) {
var optionTally by remember { mutableStateOf(Pair(BigDecimal.ZERO, defaultColor)) } Color.Green.copy(alpha = 0.32f)
} else {
LaunchedEffect(key1 = optionNumber, key2 = pollViewModel) { MaterialTheme.colors.primary.copy(alpha = 0.32f)
withContext(Dispatchers.IO) {
val myTally = pollViewModel.optionVoteTally(optionNumber)
val color = if (
pollViewModel.consensusThreshold != null &&
myTally >= pollViewModel.consensusThreshold!!
) {
Color.Green.copy(alpha = 0.32f)
} else {
defaultColor
}
if (myTally > optionTally.first || color != optionTally.second) {
optionTally = Pair(myTally, color)
}
}
} }
ZapVote( ZapVote(
baseNote, baseNote,
poolOption,
accountViewModel, accountViewModel,
pollViewModel, pollViewModel,
optionNumber,
nonClickablePrepend = { nonClickablePrepend = {
Box( Box(
Modifier Modifier
@@ -121,14 +131,14 @@ private fun OptionNote(
.clip(shape = RoundedCornerShape(15.dp)) .clip(shape = RoundedCornerShape(15.dp))
.border( .border(
2.dp, 2.dp,
optionTally.second, color,
RoundedCornerShape(15.dp) RoundedCornerShape(15.dp)
) )
) { ) {
LinearProgressIndicator( LinearProgressIndicator(
modifier = Modifier.matchParentSize(), modifier = Modifier.matchParentSize(),
color = optionTally.second, color = color,
progress = optionTally.first.toFloat() progress = poolOption.tally.toFloat()
) )
Row( Row(
@@ -141,7 +151,7 @@ private fun OptionNote(
.width(40.dp) .width(40.dp)
) { ) {
Text( Text(
text = "${(optionTally.first.toFloat() * 100).roundToInt()}%", text = "${(poolOption.tally.toFloat() * 100).roundToInt()}%",
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
} }
@@ -152,7 +162,7 @@ private fun OptionNote(
.padding(15.dp) .padding(15.dp)
) { ) {
TranslatableRichTextViewer( TranslatableRichTextViewer(
optionText, poolOption.descriptor,
canPreview, canPreview,
Modifier, Modifier,
pollViewModel.pollEvent?.tags(), pollViewModel.pollEvent?.tags(),
@@ -170,9 +180,9 @@ private fun OptionNote(
} else { } else {
ZapVote( ZapVote(
baseNote, baseNote,
poolOption,
accountViewModel, accountViewModel,
pollViewModel, pollViewModel,
optionNumber,
nonClickablePrepend = {}, nonClickablePrepend = {},
clickablePrepend = { clickablePrepend = {
Box( Box(
@@ -186,7 +196,7 @@ private fun OptionNote(
) )
) { ) {
TranslatableRichTextViewer( TranslatableRichTextViewer(
optionText, poolOption.descriptor,
canPreview, canPreview,
Modifier.padding(15.dp), Modifier.padding(15.dp),
pollViewModel.pollEvent?.tags(), pollViewModel.pollEvent?.tags(),
@@ -205,9 +215,9 @@ private fun OptionNote(
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
fun ZapVote( fun ZapVote(
baseNote: Note, baseNote: Note,
poolOption: PollOption,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
pollViewModel: PollNoteViewModel, pollViewModel: PollNoteViewModel,
pollOption: Int,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
nonClickablePrepend: @Composable () -> Unit, nonClickablePrepend: @Composable () -> Unit,
clickablePrepend: @Composable () -> Unit clickablePrepend: @Composable () -> Unit
@@ -264,7 +274,7 @@ fun ZapVote(
) )
.show() .show()
} }
} else if (pollViewModel.isVoteAmountAtomic() && pollViewModel.isPollOptionZappedBy(pollOption, accountViewModel.userProfile())) { } else if (pollViewModel.isVoteAmountAtomic() && poolOption.zappedByLoggedIn) {
// only allow one vote per option when min==max, i.e. atomic vote amount specified // only allow one vote per option when min==max, i.e. atomic vote amount specified
scope.launch { scope.launch {
Toast Toast
@@ -281,7 +291,7 @@ fun ZapVote(
accountViewModel.zap( accountViewModel.zap(
baseNote, baseNote,
account.zapAmountChoices.first() * 1000, account.zapAmountChoices.first() * 1000,
pollOption, poolOption.option,
"", "",
context, context,
onError = { onError = {
@@ -311,7 +321,7 @@ fun ZapVote(
baseNote, baseNote,
accountViewModel, accountViewModel,
pollViewModel, pollViewModel,
pollOption, poolOption.option,
onDismiss = { onDismiss = {
wantsToZap = false wantsToZap = false
zappingProgress = 0f zappingProgress = 0f
@@ -335,17 +345,7 @@ fun ZapVote(
clickablePrepend() clickablePrepend()
var optionWasZappedByLoggedInUser by remember { mutableStateOf(false) } if (poolOption.zappedByLoggedIn) {
LaunchedEffect(key1 = zapsState) {
withContext(Dispatchers.IO) {
if (!optionWasZappedByLoggedInUser) {
optionWasZappedByLoggedInUser = pollViewModel.isPollOptionZappedBy(pollOption, accountViewModel.userProfile())
}
}
}
if (optionWasZappedByLoggedInUser) {
zappingProgress = 1f zappingProgress = 1f
Icon( Icon(
imageVector = Icons.Default.Bolt, imageVector = Icons.Default.Bolt,
@@ -372,20 +372,10 @@ fun ZapVote(
} }
} }
var wasZappedByLoggedInUser by remember { mutableStateOf(false) }
LaunchedEffect(key1 = zapsState) {
withContext(Dispatchers.IO) {
if (!wasZappedByLoggedInUser) {
wasZappedByLoggedInUser = accountViewModel.calculateIfNoteWasZappedByAccount(zappedNote)
}
}
}
// only show tallies after a user has zapped note // only show tallies after a user has zapped note
if (baseNote.author == accountViewModel.userProfile() || wasZappedByLoggedInUser) { if (baseNote.author == accountViewModel.userProfile() || pollViewModel.wasZappedByLoggedInAccount) {
Text( Text(
showAmount(pollViewModel.zappedPollOptionAmount(pollOption)), showAmount(poolOption.zappedValue),
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
@@ -1,5 +1,9 @@
package com.vitorpamplona.amethyst.ui.note package com.vitorpamplona.amethyst.ui.note
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
@@ -8,19 +12,30 @@ import java.math.BigDecimal
import java.math.RoundingMode import java.math.RoundingMode
import java.util.* import java.util.*
class PollNoteViewModel { data class PollOption(
val option: Int,
val descriptor: String,
val zappedValue: BigDecimal,
val tally: BigDecimal,
val consensusThreadhold: Boolean,
val zappedByLoggedIn: Boolean
)
class PollNoteViewModel : ViewModel() {
var account: Account? = null var account: Account? = null
private var pollNote: Note? = null private var pollNote: Note? = null
var pollEvent: PollNoteEvent? = null var pollEvent: PollNoteEvent? = null
private var pollOptions: Map<Int, String>? = null var pollOptions: Map<Int, String>? = null
var valueMaximum: Int? = null var valueMaximum: Int? = null
var valueMinimum: Int? = null var valueMinimum: Int? = null
private var closedAt: Int? = null private var closedAt: Int? = null
var consensusThreshold: BigDecimal? = null var consensusThreshold: BigDecimal? = null
var totalZapped: BigDecimal = BigDecimal.ZERO var totalZapped: BigDecimal = BigDecimal.ZERO
var wasZappedByAuthor: Boolean = false var wasZappedByLoggedInAccount: Boolean = false
var tallies by mutableStateOf<List<PollOption>>(emptyList())
fun load(acc: Account, note: Note?) { fun load(acc: Account, note: Note?) {
account = acc account = acc
@@ -32,15 +47,35 @@ class PollNoteViewModel {
consensusThreshold = pollEvent?.getTagInt(CONSENSUS_THRESHOLD)?.toFloat()?.div(100)?.toBigDecimal() consensusThreshold = pollEvent?.getTagInt(CONSENSUS_THRESHOLD)?.toFloat()?.div(100)?.toBigDecimal()
closedAt = pollEvent?.getTagInt(CLOSED_AT) closedAt = pollEvent?.getTagInt(CLOSED_AT)
refreshTallies()
}
fun refreshTallies() {
totalZapped = totalZapped() totalZapped = totalZapped()
wasZappedByAuthor = note?.let { account?.calculateIfNoteWasZappedByAccount(it) } ?: false wasZappedByLoggedInAccount = pollNote?.let { account?.calculateIfNoteWasZappedByAccount(it) } ?: false
tallies = pollOptions?.keys?.map {
val zappedInOption = zappedPollOptionAmount(it)
val myTally = if (totalZapped.compareTo(BigDecimal.ZERO) > 0) {
zappedInOption.divide(totalZapped, 2, RoundingMode.HALF_UP)
} else {
BigDecimal.ZERO
}
val zappedByLoggedIn = account?.userProfile()?.let { it1 -> isPollOptionZappedBy(it, it1) } ?: false
val consensus = consensusThreshold != null && myTally >= consensusThreshold!!
PollOption(it, pollOptions?.get(it) ?: "", zappedInOption, myTally, consensus, zappedByLoggedIn)
} ?: emptyList()
} }
fun canZap(): Boolean { fun canZap(): Boolean {
val account = account ?: return false val account = account ?: return false
val user = account.userProfile() ?: return false val user = account.userProfile() ?: return false
val note = pollNote ?: return false val note = pollNote ?: return false
return user != note.author && !wasZappedByAuthor return user != note.author && !wasZappedByLoggedInAccount
} }
fun isVoteAmountAtomic() = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum fun isVoteAmountAtomic() = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum
@@ -88,14 +123,6 @@ class PollNoteViewModel {
return false return false
} }
fun optionVoteTally(op: Int): BigDecimal {
return if (totalZapped.compareTo(BigDecimal.ZERO) > 0) {
zappedPollOptionAmount(op).divide(totalZapped, 2, RoundingMode.HALF_UP)
} else {
BigDecimal.ZERO
}
}
fun isPollOptionZappedBy(option: Int, user: User): Boolean { fun isPollOptionZappedBy(option: Int, user: User): Boolean {
return pollNote!!.zaps return pollNote!!.zaps
.any { .any {
@@ -105,7 +132,7 @@ class PollNoteViewModel {
} }
} }
fun zappedPollOptionAmount(option: Int): BigDecimal { private fun zappedPollOptionAmount(option: Int): BigDecimal {
return pollNote?.zaps?.values?.sumOf { return pollNote?.zaps?.values?.sumOf {
val event = it?.event as? LnZapEvent val event = it?.event as? LnZapEvent
if (event?.zappedPollOption() == option) { if (event?.zappedPollOption() == option) {
@@ -116,7 +143,7 @@ class PollNoteViewModel {
} ?: BigDecimal(0) } ?: BigDecimal(0)
} }
fun totalZapped(): BigDecimal { private fun totalZapped(): BigDecimal {
return pollNote?.zaps?.values?.sumOf { return pollNote?.zaps?.values?.sumOf {
val zapEvent = (it?.event as? LnZapEvent) val zapEvent = (it?.event as? LnZapEvent)