From f84501e40f777f35c74c79ebdd74a01598794971 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 14 May 2023 17:03:46 -0400 Subject: [PATCH] Improvements to Poll Caching system: Fixes https://github.com/vitorpamplona/amethyst/issues/406 --- .../com/vitorpamplona/amethyst/model/Note.kt | 6 +- .../amethyst/ui/components/BundledUpdate.kt | 6 +- .../amethyst/ui/note/PollNote.kt | 122 ++++++++---------- .../amethyst/ui/note/PollNoteViewModel.kt | 57 +++++--- 4 files changed, 104 insertions(+), 87 deletions(-) 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 c391efeb6..e8920e69f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -411,9 +411,9 @@ class NoteLiveSet(u: Note) { class NoteLiveData(val note: Note) : LiveData(NoteState(note)) { // Refreshes observers in batches. private val bundler = BundledUpdate(300, Dispatchers.IO) { - if (hasActiveObservers()) { - postValue(NoteState(note)) - } + // if (hasObservers()) { + postValue(NoteState(note)) + // } } fun invalidateData() { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/BundledUpdate.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/BundledUpdate.kt index 05651dcbc..2a1036fb9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/BundledUpdate.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/BundledUpdate.kt @@ -71,9 +71,9 @@ class BundledInsert( delay(delay) - mySet.clear() - queue.drainTo(mySet) - onUpdate(mySet) + val mySet2 = mutableSetOf() + queue.drainTo(mySet2) + onUpdate(mySet2) } finally { withContext(NonCancellable) { onlyOneInBlock.set(false) 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 d5cbcd80b..12342c292 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 @@ -28,6 +28,7 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.Popup +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note @@ -37,7 +38,6 @@ import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.math.BigDecimal import java.util.* import kotlin.math.roundToInt @@ -49,19 +49,45 @@ fun PollNote( accountViewModel: AccountViewModel, navController: NavController ) { - val zapsState by baseNote.live().zaps.observeAsState() - val zappedNote = zapsState?.note ?: return - val accountState by accountViewModel.accountLiveData.observeAsState() - val account = accountState?.account ?: return + val account = remember(accountState) { accountState?.account } ?: return - val pollViewModel = PollNoteViewModel() - pollViewModel.load(account, zappedNote) + val pollViewModel: PollNoteViewModel = viewModel() - 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( - poll_op.key, - poll_op.value, + poll_op, pollViewModel, baseNote, accountViewModel, @@ -74,8 +100,7 @@ fun PollNote( @Composable private fun OptionNote( - optionNumber: Int, - optionText: String, + poolOption: PollOption, pollViewModel: PollNoteViewModel, baseNote: Note, accountViewModel: AccountViewModel, @@ -88,32 +113,17 @@ private fun OptionNote( modifier = Modifier.padding(vertical = 3.dp) ) { if (!pollViewModel.canZap()) { - val defaultColor = MaterialTheme.colors.primary.copy(alpha = 0.32f) - var optionTally by remember { mutableStateOf(Pair(BigDecimal.ZERO, defaultColor)) } - - LaunchedEffect(key1 = optionNumber, key2 = pollViewModel) { - 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) - } - } + val color = if (poolOption.consensusThreadhold) { + Color.Green.copy(alpha = 0.32f) + } else { + MaterialTheme.colors.primary.copy(alpha = 0.32f) } ZapVote( baseNote, + poolOption, accountViewModel, pollViewModel, - optionNumber, nonClickablePrepend = { Box( Modifier @@ -121,14 +131,14 @@ private fun OptionNote( .clip(shape = RoundedCornerShape(15.dp)) .border( 2.dp, - optionTally.second, + color, RoundedCornerShape(15.dp) ) ) { LinearProgressIndicator( modifier = Modifier.matchParentSize(), - color = optionTally.second, - progress = optionTally.first.toFloat() + color = color, + progress = poolOption.tally.toFloat() ) Row( @@ -141,7 +151,7 @@ private fun OptionNote( .width(40.dp) ) { Text( - text = "${(optionTally.first.toFloat() * 100).roundToInt()}%", + text = "${(poolOption.tally.toFloat() * 100).roundToInt()}%", fontWeight = FontWeight.Bold ) } @@ -152,7 +162,7 @@ private fun OptionNote( .padding(15.dp) ) { TranslatableRichTextViewer( - optionText, + poolOption.descriptor, canPreview, Modifier, pollViewModel.pollEvent?.tags(), @@ -170,9 +180,9 @@ private fun OptionNote( } else { ZapVote( baseNote, + poolOption, accountViewModel, pollViewModel, - optionNumber, nonClickablePrepend = {}, clickablePrepend = { Box( @@ -186,7 +196,7 @@ private fun OptionNote( ) ) { TranslatableRichTextViewer( - optionText, + poolOption.descriptor, canPreview, Modifier.padding(15.dp), pollViewModel.pollEvent?.tags(), @@ -205,9 +215,9 @@ private fun OptionNote( @OptIn(ExperimentalFoundationApi::class) fun ZapVote( baseNote: Note, + poolOption: PollOption, accountViewModel: AccountViewModel, pollViewModel: PollNoteViewModel, - pollOption: Int, modifier: Modifier = Modifier, nonClickablePrepend: @Composable () -> Unit, clickablePrepend: @Composable () -> Unit @@ -264,7 +274,7 @@ fun ZapVote( ) .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 scope.launch { Toast @@ -281,7 +291,7 @@ fun ZapVote( accountViewModel.zap( baseNote, account.zapAmountChoices.first() * 1000, - pollOption, + poolOption.option, "", context, onError = { @@ -311,7 +321,7 @@ fun ZapVote( baseNote, accountViewModel, pollViewModel, - pollOption, + poolOption.option, onDismiss = { wantsToZap = false zappingProgress = 0f @@ -335,17 +345,7 @@ fun ZapVote( clickablePrepend() - var optionWasZappedByLoggedInUser by remember { mutableStateOf(false) } - - LaunchedEffect(key1 = zapsState) { - withContext(Dispatchers.IO) { - if (!optionWasZappedByLoggedInUser) { - optionWasZappedByLoggedInUser = pollViewModel.isPollOptionZappedBy(pollOption, accountViewModel.userProfile()) - } - } - } - - if (optionWasZappedByLoggedInUser) { + if (poolOption.zappedByLoggedIn) { zappingProgress = 1f Icon( 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 - if (baseNote.author == accountViewModel.userProfile() || wasZappedByLoggedInUser) { + if (baseNote.author == accountViewModel.userProfile() || pollViewModel.wasZappedByLoggedInAccount) { Text( - showAmount(pollViewModel.zappedPollOptionAmount(pollOption)), + showAmount(poolOption.zappedValue), fontSize = 14.sp, color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), modifier = modifier diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt index 73c273806..fa142446b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt @@ -1,5 +1,9 @@ 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.Note import com.vitorpamplona.amethyst.model.User @@ -8,19 +12,30 @@ import java.math.BigDecimal import java.math.RoundingMode 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 private var pollNote: Note? = null var pollEvent: PollNoteEvent? = null - private var pollOptions: Map? = null + var pollOptions: Map? = null var valueMaximum: Int? = null var valueMinimum: Int? = null private var closedAt: Int? = null var consensusThreshold: BigDecimal? = null var totalZapped: BigDecimal = BigDecimal.ZERO - var wasZappedByAuthor: Boolean = false + var wasZappedByLoggedInAccount: Boolean = false + + var tallies by mutableStateOf>(emptyList()) fun load(acc: Account, note: Note?) { account = acc @@ -32,15 +47,35 @@ class PollNoteViewModel { consensusThreshold = pollEvent?.getTagInt(CONSENSUS_THRESHOLD)?.toFloat()?.div(100)?.toBigDecimal() closedAt = pollEvent?.getTagInt(CLOSED_AT) + refreshTallies() + } + + fun refreshTallies() { 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 { val account = account ?: return false val user = account.userProfile() ?: 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 @@ -88,14 +123,6 @@ class PollNoteViewModel { 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 { return pollNote!!.zaps .any { @@ -105,7 +132,7 @@ class PollNoteViewModel { } } - fun zappedPollOptionAmount(option: Int): BigDecimal { + private fun zappedPollOptionAmount(option: Int): BigDecimal { return pollNote?.zaps?.values?.sumOf { val event = it?.event as? LnZapEvent if (event?.zappedPollOption() == option) { @@ -116,7 +143,7 @@ class PollNoteViewModel { } ?: BigDecimal(0) } - fun totalZapped(): BigDecimal { + private fun totalZapped(): BigDecimal { return pollNote?.zaps?.values?.sumOf { val zapEvent = (it?.event as? LnZapEvent)