Improvements to Poll Caching system: Fixes https://github.com/vitorpamplona/amethyst/issues/406
This commit is contained in:
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user