From 3eb832c4e09cdfa9bd2d33bef74ad904e99f58f2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 31 May 2023 20:08:07 -0400 Subject: [PATCH] Improves Poll Rendering speed. --- .../amethyst/ui/components/RichTextViewer.kt | 43 +- .../amethyst/ui/note/MultiSetCompose.kt | 20 +- .../amethyst/ui/note/NoteCompose.kt | 59 +-- .../amethyst/ui/note/PollNote.kt | 368 +++++++----------- .../amethyst/ui/note/PollNoteViewModel.kt | 74 ++-- .../amethyst/ui/screen/CardFeedView.kt | 6 +- 6 files changed, 261 insertions(+), 309 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 0d81b568a..385be4a35 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -45,7 +45,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.uriToRoute import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import java.net.MalformedURLException import java.net.URISyntaxException import java.net.URL @@ -128,10 +127,8 @@ private fun RenderRegular( mutableStateOf(RichTextViewerState(content, emptySet(), emptyMap(), emptyList(), emptyMap())) } - val scope = rememberCoroutineScope() - LaunchedEffect(key1 = content) { - scope.launch(Dispatchers.IO) { + launch(Dispatchers.IO) { val urls = UrlDetector(content, UrlDetectorOptions.Default).detect() val urlSet = urls.mapTo(LinkedHashSet(urls.size)) { it.originalUrl } val imagesForPager = urlSet.mapNotNull { fullUrl -> @@ -363,14 +360,14 @@ private fun RenderContentAsMarkdown(content: String, backgroundColor: Color, tag var refresh by remember(content) { mutableStateOf(0) } LaunchedEffect(key1 = content) { - withContext(Dispatchers.IO) { + launch(Dispatchers.IO) { nip19References = returnNIP19References(content, tags) markdownWithSpecialContent = returnMarkdownWithSpecialContent(content, tags) } } LaunchedEffect(key1 = refresh) { - withContext(Dispatchers.IO) { + launch(Dispatchers.IO) { val newMarkdownWithSpecialContent = returnMarkdownWithSpecialContent(content, tags) if (markdownWithSpecialContent != newMarkdownWithSpecialContent) { markdownWithSpecialContent = newMarkdownWithSpecialContent @@ -425,7 +422,7 @@ private fun ObserveNIP19Event( var baseNote by remember(it) { mutableStateOf(null) } LaunchedEffect(key1 = it.hex) { - withContext(Dispatchers.IO) { + launch(Dispatchers.IO) { if (it.type == Nip19.Type.NOTE || it.type == Nip19.Type.EVENT || it.type == Nip19.Type.ADDRESS) { LocalCache.checkGetOrCreateNote(it.hex)?.let { note -> baseNote = note @@ -436,9 +433,12 @@ private fun ObserveNIP19Event( baseNote?.let { note -> val noteState by note.live().metadata.observeAsState() - if (noteState?.note?.event != null) { - LaunchedEffect(key1 = noteState) { - onRefresh() + + LaunchedEffect(key1 = noteState) { + if (noteState?.note?.event != null) { + launch(Dispatchers.IO) { + onRefresh() + } } } } @@ -452,7 +452,7 @@ private fun ObserveNIP19User( var baseUser by remember(it) { mutableStateOf(null) } LaunchedEffect(key1 = it.hex) { - withContext(Dispatchers.IO) { + launch(Dispatchers.IO) { if (it.type == Nip19.Type.USER) { LocalCache.checkGetOrCreateUser(it.hex)?.let { user -> baseUser = user @@ -463,9 +463,12 @@ private fun ObserveNIP19User( baseUser?.let { user -> val userState by user.live().metadata.observeAsState() - if (userState?.user?.info != null) { - LaunchedEffect(key1 = userState) { - onRefresh() + + LaunchedEffect(key1 = userState) { + if (userState?.user?.info != null) { + launch(Dispatchers.IO) { + onRefresh() + } } } } @@ -622,10 +625,8 @@ fun BechLink(word: String, canPreview: Boolean, backgroundColor: Color, accountV var nip19Route by remember { mutableStateOf(null) } var baseNotePair by remember { mutableStateOf?>(null) } - val scope = rememberCoroutineScope() - LaunchedEffect(key1 = word) { - scope.launch(Dispatchers.IO) { + launch(Dispatchers.IO) { Nip19.uriToRoute(word)?.let { if (it.type == Nip19.Type.NOTE || it.type == Nip19.Type.EVENT || it.type == Nip19.Type.ADDRESS) { LocalCache.checkGetOrCreateNote(it.hex)?.let { note -> @@ -675,10 +676,8 @@ fun BechLink(word: String, canPreview: Boolean, backgroundColor: Color, accountV fun HashTag(word: String, nav: (String) -> Unit) { var tagSuffixPair by remember { mutableStateOf?>(null) } - val scope = rememberCoroutineScope() - LaunchedEffect(key1 = word) { - scope.launch(Dispatchers.IO) { + launch(Dispatchers.IO) { val hashtagMatcher = hashTagsPattern.matcher(word) val (myTag, mySuffix) = try { @@ -755,10 +754,8 @@ fun TagLink(word: String, tags: List>, canPreview: Boolean, backgro var baseUserPair by remember { mutableStateOf?>(null) } var baseNotePair by remember { mutableStateOf?>(null) } - val scope = rememberCoroutineScope() - LaunchedEffect(key1 = word) { - scope.launch(Dispatchers.IO) { + launch(Dispatchers.IO) { val matcher = tagIndex.matcher(word) val (index, suffix) = try { matcher.find() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index 5fa6c3752..e3b6996ca 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -262,9 +262,7 @@ fun AuthorGalleryZaps( Column(modifier = Modifier.padding(start = 10.dp)) { FlowRow() { authorNotes.forEach { - Box() { - AuthorPictureAndComment(it.key, it.value, backgroundColor, nav, accountViewModel) - } + AuthorPictureAndComment(it.key, it.value, backgroundColor, nav, accountViewModel) } } } @@ -329,17 +327,9 @@ private fun AuthorPictureAndComment( ) { val authorPictureModifier = remember { Modifier } - val modifier = remember(comment) { - if (comment != null) { - Modifier - .fillMaxWidth() - .clickable { - nav(route) - } - } else { - Modifier.clickable { - nav(route) - } + val modifier = remember { + Modifier.clickable { + nav(route) } } @@ -381,7 +371,7 @@ private fun AuthorPictureAndComment( content = it, canPreview = true, tags = null, - modifier = Modifier.weight(1f), + modifier = remember { Modifier.fillMaxWidth() }, backgroundColor = backgroundColor, accountViewModel = accountViewModel, nav = nav diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index fa572ece4..6d5db9f43 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -604,7 +604,7 @@ private fun RenderPoll( nav: (String) -> Unit ) { val noteEvent = note.event as? PollNoteEvent ?: return - val eventContent = noteEvent.content() + val eventContent = remember { noteEvent.content() } if (makeItShort && accountViewModel.isLoggedUser(note.author)) { Text( @@ -2249,35 +2249,42 @@ fun UserPicture( .align(Alignment.TopEnd) } - val myIconBackgroundModifier = remember { - Modifier - .clip(CircleShape) - .fillMaxSize(0.6f) - .align(Alignment.Center) - } - - val myIconModifier = remember { - Modifier - .width(size.div(3.5f)) - .height(size.div(3.5f)) - } - - Box(myIconBoxModifier, contentAlignment = Alignment.Center) { - Box( - myIconBackgroundModifier.background(MaterialTheme.colors.background) - ) - - Icon( - painter = painterResource(R.drawable.ic_verified), - stringResource(id = R.string.following), - modifier = myIconModifier, - tint = Following - ) - } + FollowingIcon(myIconBoxModifier, size) } } } +@Composable +private fun FollowingIcon( + myIconBoxModifier: Modifier, + size: Dp +) { + Box(myIconBoxModifier, contentAlignment = Alignment.Center) { + val myIconBackgroundModifier = remember { + Modifier + .clip(CircleShape) + .fillMaxSize(0.6f) + } + + Box( + myIconBackgroundModifier.background(MaterialTheme.colors.background) + ) + + val myIconModifier = remember { + Modifier + .width(size.div(3.5f)) + .height(size.div(3.5f)) + } + + Icon( + painter = painterResource(R.drawable.ic_verified), + stringResource(id = R.string.following), + modifier = myIconModifier, + tint = Following + ) + } +} + data class DropDownParams( val isFollowingAuthor: Boolean, val isPrivateBookmarkNote: Boolean, 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 ca545bb3f..85d426cb6 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 @@ -4,14 +4,12 @@ import android.widget.Toast import androidx.compose.foundation.* 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 import androidx.compose.material.icons.outlined.Bolt import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -20,23 +18,20 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.font.FontWeight -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.Dialog -import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.Popup import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.model.LnZapEvent import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import java.util.* import kotlin.math.roundToInt @@ -48,16 +43,11 @@ fun PollNote( accountViewModel: AccountViewModel, nav: (String) -> Unit ) { - val accountState by accountViewModel.accountLiveData.observeAsState() - val account = remember(accountState) { accountState?.account } ?: return - val pollViewModel: PollNoteViewModel = viewModel( key = baseNote.idHex ) - LaunchedEffect(key1 = baseNote) { - pollViewModel.load(account, baseNote) - } + pollViewModel.load(accountViewModel.account, baseNote) PollNote( baseNote = baseNote, @@ -78,15 +68,11 @@ fun PollNote( accountViewModel: AccountViewModel, nav: (String) -> Unit ) { - val zapsState by baseNote.live().zaps.observeAsState() + WatchZapsAndUpdateTallies(baseNote, pollViewModel) - LaunchedEffect(key1 = zapsState) { - withContext(Dispatchers.IO) { - pollViewModel.refreshTallies() - } - } + val tallies by pollViewModel.tallies.collectAsState() - pollViewModel.tallies.forEach { poll_op -> + tallies.forEach { poll_op -> OptionNote( poll_op, pollViewModel, @@ -99,6 +85,21 @@ fun PollNote( } } +@Composable +private fun WatchZapsAndUpdateTallies( + baseNote: Note, + pollViewModel: PollNoteViewModel +) { + val zapsState by baseNote.live().zaps.observeAsState() + val scope = rememberCoroutineScope() + + LaunchedEffect(key1 = zapsState) { + scope.launch(Dispatchers.IO) { + pollViewModel.refreshTallies() + } + } +} + @Composable private fun OptionNote( poolOption: PollOption, @@ -109,6 +110,8 @@ private fun OptionNote( backgroundColor: Color, nav: (String) -> Unit ) { + val tags = remember { baseNote.event?.tags() } + Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 3.dp) @@ -126,54 +129,16 @@ private fun OptionNote( accountViewModel, pollViewModel, nonClickablePrepend = { - Box( - Modifier - .fillMaxWidth(0.75f) - .clip(shape = RoundedCornerShape(15.dp)) - .border( - 2.dp, - color, - RoundedCornerShape(15.dp) - ) - ) { - LinearProgressIndicator( - modifier = Modifier.matchParentSize(), - color = color, - progress = poolOption.tally.toFloat() - ) - - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Column( - horizontalAlignment = Alignment.End, - modifier = Modifier - .padding(horizontal = 10.dp) - .width(40.dp) - ) { - Text( - text = "${(poolOption.tally.toFloat() * 100).roundToInt()}%", - fontWeight = FontWeight.Bold - ) - } - - Column( - modifier = Modifier - .fillMaxWidth() - .padding(15.dp) - ) { - TranslatableRichTextViewer( - poolOption.descriptor, - canPreview, - Modifier, - pollViewModel.pollEvent?.tags(), - backgroundColor, - accountViewModel, - nav - ) - } - } - } + RenderOptionAfterVote( + poolOption.descriptor, + poolOption.tally.toFloat(), + color, + canPreview, + tags, + backgroundColor, + accountViewModel, + nav + ) }, clickablePrepend = { } @@ -186,32 +151,113 @@ private fun OptionNote( pollViewModel, nonClickablePrepend = {}, clickablePrepend = { - Box( - Modifier - .fillMaxWidth(0.75f) - .clip(shape = RoundedCornerShape(15.dp)) - .border( - 2.dp, - MaterialTheme.colors.primary, - RoundedCornerShape(15.dp) - ) - ) { - TranslatableRichTextViewer( - poolOption.descriptor, - canPreview, - Modifier.padding(15.dp), - pollViewModel.pollEvent?.tags(), - backgroundColor, - accountViewModel, - nav - ) - } + RenderOptionBeforeVote(poolOption.descriptor, canPreview, tags, backgroundColor, accountViewModel, nav) } ) } } } +@Composable +private fun RenderOptionAfterVote( + description: String, + totalRatio: Float, + color: Color, + canPreview: Boolean, + tags: List>?, + backgroundColor: Color, + accountViewModel: AccountViewModel, + nav: (String) -> Unit +) { + val totalPercentage = remember(totalRatio) { + "${(totalRatio * 100).roundToInt()}%" + } + + Box( + Modifier + .fillMaxWidth(0.75f) + .clip(shape = RoundedCornerShape(15.dp)) + .border( + 2.dp, + color, + RoundedCornerShape(15.dp) + ) + ) { + LinearProgressIndicator( + modifier = Modifier.matchParentSize(), + color = color, + progress = totalRatio + ) + + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Column( + horizontalAlignment = Alignment.End, + modifier = remember { + Modifier + .padding(horizontal = 10.dp) + .width(40.dp) + } + ) { + Text( + text = totalPercentage, + fontWeight = FontWeight.Bold + ) + } + + Column( + modifier = remember { + Modifier + .fillMaxWidth() + .padding(15.dp) + } + ) { + TranslatableRichTextViewer( + description, + canPreview, + remember { Modifier }, + tags, + backgroundColor, + accountViewModel, + nav + ) + } + } + } +} + +@Composable +private fun RenderOptionBeforeVote( + description: String, + canPreview: Boolean, + tags: List>?, + backgroundColor: Color, + accountViewModel: AccountViewModel, + nav: (String) -> Unit +) { + Box( + Modifier + .fillMaxWidth(0.75f) + .clip(shape = RoundedCornerShape(15.dp)) + .border( + 2.dp, + MaterialTheme.colors.primary, + RoundedCornerShape(15.dp) + ) + ) { + TranslatableRichTextViewer( + description, + canPreview, + remember { Modifier.padding(15.dp) }, + tags, + backgroundColor, + accountViewModel, + nav + ) + } +} + @Composable @OptIn(ExperimentalFoundationApi::class) fun ZapVote( @@ -374,7 +420,7 @@ fun ZapVote( } // only show tallies after a user has zapped note - if (baseNote.author == accountViewModel.userProfile() || pollViewModel.wasZappedByLoggedInAccount) { + if (!pollViewModel.canZap()) { Text( showAmount(poolOption.zappedValue), fontSize = 14.sp, @@ -399,23 +445,18 @@ fun FilteredZapAmountChoicePopup( val context = LocalContext.current val accountState by accountViewModel.accountLiveData.observeAsState() - val account = accountState?.account ?: return + val defaultZapType by remember(accountState) { + derivedStateOf { + accountState?.account?.defaultZapType ?: LnZapEvent.ZapType.PRIVATE + } + } + val zapMessage = "" val scope = rememberCoroutineScope() - val options = account.zapAmountChoices.filter { pollViewModel.isValidInputVoteAmount(it) }.toMutableList() - if (options.isEmpty()) { - pollViewModel.valueMinimum?.let { minimum -> - pollViewModel.valueMaximum?.let { maximum -> - if (minimum != maximum) { - options.add(((minimum + maximum) / 2).toLong()) - } - } - } + val sortedOptions = remember(accountState) { + pollViewModel.createZapOptionsThatMatchThePollingParameters() } - pollViewModel.valueMinimum?.let { options.add(it.toLong()) } - pollViewModel.valueMaximum?.let { options.add(it.toLong()) } - val sortedOptions = options.toSet().sorted() Popup( alignment = Alignment.BottomCenter, @@ -424,6 +465,10 @@ fun FilteredZapAmountChoicePopup( ) { FlowRow(horizontalArrangement = Arrangement.Center) { sortedOptions.forEach { amountInSats -> + val zapAmount = remember { + "⚡ ${showAmount(amountInSats.toBigDecimal().setScale(1))}" + } + Button( modifier = Modifier.padding(horizontal = 3.dp), onClick = { @@ -436,7 +481,7 @@ fun FilteredZapAmountChoicePopup( context, onError, onProgress, - account.defaultZapType + defaultZapType ) onDismiss() } @@ -448,7 +493,7 @@ fun FilteredZapAmountChoicePopup( ) ) { Text( - "⚡ ${showAmount(amountInSats.toBigDecimal().setScale(1))}", + text = zapAmount, color = Color.White, textAlign = TextAlign.Center, modifier = Modifier.combinedClickable( @@ -462,7 +507,7 @@ fun FilteredZapAmountChoicePopup( context, onError, onProgress, - account.defaultZapType + defaultZapType ) onDismiss() } @@ -477,118 +522,3 @@ fun FilteredZapAmountChoicePopup( } } } - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun ZapVoteAmountChoicePopup( - baseNote: Note, - accountViewModel: AccountViewModel, - pollViewModel: PollNoteViewModel, - pollOption: Int, - onDismiss: () -> Unit, - onError: (text: String) -> Unit, - onProgress: (percent: Float) -> Unit -) { - val context = LocalContext.current - - var inputAmountText by rememberSaveable { mutableStateOf("") } - - 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 - ) - ) { - Surface { - Row( - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .padding(10.dp) - ) { - var amount = pollViewModel.inputVoteAmountLong(inputAmountText) - - // only prompt for input amount if vote is not atomic - if (!pollViewModel.isVoteAmountAtomic()) { - OutlinedTextField( - value = inputAmountText, - onValueChange = { inputAmountText = it }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.width(150.dp), - colors = if (pollViewModel.isValidInputVoteAmount(amount)) colorValid else colorInValid, - label = { - Text( - text = stringResource(R.string.poll_zap_amount), - color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) - ) - }, - placeholder = { - Text( - text = pollViewModel.voteAmountPlaceHolderText(context.getString(R.string.sats)), - color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) - ) - } - ) - } else { amount = pollViewModel.valueMaximum?.toLong() } - - val isValidInputAmount = pollViewModel.isValidInputVoteAmount(amount) - Button( - modifier = Modifier.padding(horizontal = 3.dp), - enabled = isValidInputAmount, - onClick = { - if (amount != null && isValidInputAmount) { - accountViewModel.zap( - baseNote, - amount * 1000, - pollOption, - "", - context, - onError, - onProgress - ) - 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 = { - if (amount != null && isValidInputAmount) { - accountViewModel.zap( - baseNote, - amount * 1000, - pollOption, - "", - context, - onError, - onProgress - ) - onDismiss() - } - }, - onLongClick = {} - ) - ) - } - } - } - } -} 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 fa142446b..76fc7a807 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,17 +1,23 @@ package com.vitorpamplona.amethyst.ui.note +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable 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.RelaySetupInfo import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.model.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import java.math.BigDecimal import java.math.RoundingMode import java.util.* +@Immutable data class PollOption( val option: Int, val descriptor: String, @@ -21,21 +27,23 @@ data class PollOption( val zappedByLoggedIn: Boolean ) +@Stable class PollNoteViewModel : ViewModel() { - var account: Account? = null + private var account: Account? = null private var pollNote: Note? = null - var pollEvent: PollNoteEvent? = null - var pollOptions: Map? = null - var valueMaximum: Int? = null - var valueMinimum: Int? = null + private var pollEvent: PollNoteEvent? = null + private var pollOptions: Map? = null + private var valueMaximum: Int? = null + private var valueMinimum: Int? = null private var closedAt: Int? = null - var consensusThreshold: BigDecimal? = null + private var consensusThreshold: BigDecimal? = null - var totalZapped: BigDecimal = BigDecimal.ZERO - var wasZappedByLoggedInAccount: Boolean = false + private var totalZapped: BigDecimal = BigDecimal.ZERO + private var wasZappedByLoggedInAccount: Boolean = false - var tallies by mutableStateOf>(emptyList()) + private val _tallies = MutableStateFlow>(emptyList()) + val tallies = _tallies.asStateFlow() fun load(acc: Account, note: Note?) { account = acc @@ -46,36 +54,35 @@ class PollNoteViewModel : ViewModel() { valueMinimum = pollEvent?.getTagInt(VALUE_MINIMUM) consensusThreshold = pollEvent?.getTagInt(CONSENSUS_THRESHOLD)?.toFloat()?.div(100)?.toBigDecimal() closedAt = pollEvent?.getTagInt(CLOSED_AT) - - refreshTallies() } - fun refreshTallies() { + suspend fun refreshTallies() { totalZapped = totalZapped() wasZappedByLoggedInAccount = pollNote?.let { account?.calculateIfNoteWasZappedByAccount(it) } ?: false - tallies = pollOptions?.keys?.map { - val zappedInOption = zappedPollOptionAmount(it) + _tallies.emit( + 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 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 zappedByLoggedIn = account?.userProfile()?.let { it1 -> isPollOptionZappedBy(it, it1) } ?: false - val consensus = consensusThreshold != null && myTally >= consensusThreshold!! + val consensus = consensusThreshold != null && myTally >= consensusThreshold!! - PollOption(it, pollOptions?.get(it) ?: "", zappedInOption, myTally, consensus, zappedByLoggedIn) - } ?: emptyList() + 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 && !wasZappedByLoggedInAccount + return account.userProfile() != note.author && !wasZappedByLoggedInAccount } fun isVoteAmountAtomic() = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum @@ -154,4 +161,21 @@ class PollNoteViewModel : ViewModel() { } } ?: BigDecimal(0) } + + fun createZapOptionsThatMatchThePollingParameters(): List { + 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.toLong()) } + valueMaximum?.let { options.add(it.toLong()) } + + return options.toSet().sorted() + } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt index c20bd9efe..1727648ec 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt @@ -153,6 +153,10 @@ private fun FeedLoaded( nav: (String) -> Unit, routeForLastRead: String ) { + val defaultModifier = remember { + Modifier.fillMaxWidth().defaultMinSize(minHeight = 100.dp) + } + LazyColumn( modifier = remember { Modifier.fillMaxSize() }, contentPadding = remember { @@ -164,7 +168,7 @@ private fun FeedLoaded( state = listState ) { itemsIndexed(state.feed.value, key = { _, item -> item.id() }) { _, item -> - Row(Modifier.fillMaxWidth().defaultMinSize(minHeight = 100.dp)) { + Row(defaultModifier) { when (item) { is NoteCard -> NoteCompose( item.note,