Improves Poll Rendering speed.

This commit is contained in:
Vitor Pamplona
2023-05-31 20:08:07 -04:00
parent a57ca2a79d
commit 3eb832c4e0
6 changed files with 261 additions and 309 deletions
@@ -45,7 +45,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.uriToRoute import com.vitorpamplona.amethyst.ui.uriToRoute
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.MalformedURLException import java.net.MalformedURLException
import java.net.URISyntaxException import java.net.URISyntaxException
import java.net.URL import java.net.URL
@@ -128,10 +127,8 @@ private fun RenderRegular(
mutableStateOf(RichTextViewerState(content, emptySet(), emptyMap(), emptyList(), emptyMap())) mutableStateOf(RichTextViewerState(content, emptySet(), emptyMap(), emptyList(), emptyMap()))
} }
val scope = rememberCoroutineScope()
LaunchedEffect(key1 = content) { LaunchedEffect(key1 = content) {
scope.launch(Dispatchers.IO) { launch(Dispatchers.IO) {
val urls = UrlDetector(content, UrlDetectorOptions.Default).detect() val urls = UrlDetector(content, UrlDetectorOptions.Default).detect()
val urlSet = urls.mapTo(LinkedHashSet(urls.size)) { it.originalUrl } val urlSet = urls.mapTo(LinkedHashSet(urls.size)) { it.originalUrl }
val imagesForPager = urlSet.mapNotNull { fullUrl -> val imagesForPager = urlSet.mapNotNull { fullUrl ->
@@ -363,14 +360,14 @@ private fun RenderContentAsMarkdown(content: String, backgroundColor: Color, tag
var refresh by remember(content) { mutableStateOf(0) } var refresh by remember(content) { mutableStateOf(0) }
LaunchedEffect(key1 = content) { LaunchedEffect(key1 = content) {
withContext(Dispatchers.IO) { launch(Dispatchers.IO) {
nip19References = returnNIP19References(content, tags) nip19References = returnNIP19References(content, tags)
markdownWithSpecialContent = returnMarkdownWithSpecialContent(content, tags) markdownWithSpecialContent = returnMarkdownWithSpecialContent(content, tags)
} }
} }
LaunchedEffect(key1 = refresh) { LaunchedEffect(key1 = refresh) {
withContext(Dispatchers.IO) { launch(Dispatchers.IO) {
val newMarkdownWithSpecialContent = returnMarkdownWithSpecialContent(content, tags) val newMarkdownWithSpecialContent = returnMarkdownWithSpecialContent(content, tags)
if (markdownWithSpecialContent != newMarkdownWithSpecialContent) { if (markdownWithSpecialContent != newMarkdownWithSpecialContent) {
markdownWithSpecialContent = newMarkdownWithSpecialContent markdownWithSpecialContent = newMarkdownWithSpecialContent
@@ -425,7 +422,7 @@ private fun ObserveNIP19Event(
var baseNote by remember(it) { mutableStateOf<Note?>(null) } var baseNote by remember(it) { mutableStateOf<Note?>(null) }
LaunchedEffect(key1 = it.hex) { 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) { if (it.type == Nip19.Type.NOTE || it.type == Nip19.Type.EVENT || it.type == Nip19.Type.ADDRESS) {
LocalCache.checkGetOrCreateNote(it.hex)?.let { note -> LocalCache.checkGetOrCreateNote(it.hex)?.let { note ->
baseNote = note baseNote = note
@@ -436,9 +433,12 @@ private fun ObserveNIP19Event(
baseNote?.let { note -> baseNote?.let { note ->
val noteState by note.live().metadata.observeAsState() val noteState by note.live().metadata.observeAsState()
if (noteState?.note?.event != null) {
LaunchedEffect(key1 = noteState) { LaunchedEffect(key1 = noteState) {
onRefresh() if (noteState?.note?.event != null) {
launch(Dispatchers.IO) {
onRefresh()
}
} }
} }
} }
@@ -452,7 +452,7 @@ private fun ObserveNIP19User(
var baseUser by remember(it) { mutableStateOf<User?>(null) } var baseUser by remember(it) { mutableStateOf<User?>(null) }
LaunchedEffect(key1 = it.hex) { LaunchedEffect(key1 = it.hex) {
withContext(Dispatchers.IO) { launch(Dispatchers.IO) {
if (it.type == Nip19.Type.USER) { if (it.type == Nip19.Type.USER) {
LocalCache.checkGetOrCreateUser(it.hex)?.let { user -> LocalCache.checkGetOrCreateUser(it.hex)?.let { user ->
baseUser = user baseUser = user
@@ -463,9 +463,12 @@ private fun ObserveNIP19User(
baseUser?.let { user -> baseUser?.let { user ->
val userState by user.live().metadata.observeAsState() val userState by user.live().metadata.observeAsState()
if (userState?.user?.info != null) {
LaunchedEffect(key1 = userState) { LaunchedEffect(key1 = userState) {
onRefresh() 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<Nip19.Return?>(null) } var nip19Route by remember { mutableStateOf<Nip19.Return?>(null) }
var baseNotePair by remember { mutableStateOf<Pair<Note, String?>?>(null) } var baseNotePair by remember { mutableStateOf<Pair<Note, String?>?>(null) }
val scope = rememberCoroutineScope()
LaunchedEffect(key1 = word) { LaunchedEffect(key1 = word) {
scope.launch(Dispatchers.IO) { launch(Dispatchers.IO) {
Nip19.uriToRoute(word)?.let { Nip19.uriToRoute(word)?.let {
if (it.type == Nip19.Type.NOTE || it.type == Nip19.Type.EVENT || it.type == Nip19.Type.ADDRESS) { if (it.type == Nip19.Type.NOTE || it.type == Nip19.Type.EVENT || it.type == Nip19.Type.ADDRESS) {
LocalCache.checkGetOrCreateNote(it.hex)?.let { note -> 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) { fun HashTag(word: String, nav: (String) -> Unit) {
var tagSuffixPair by remember { mutableStateOf<Pair<String, String?>?>(null) } var tagSuffixPair by remember { mutableStateOf<Pair<String, String?>?>(null) }
val scope = rememberCoroutineScope()
LaunchedEffect(key1 = word) { LaunchedEffect(key1 = word) {
scope.launch(Dispatchers.IO) { launch(Dispatchers.IO) {
val hashtagMatcher = hashTagsPattern.matcher(word) val hashtagMatcher = hashTagsPattern.matcher(word)
val (myTag, mySuffix) = try { val (myTag, mySuffix) = try {
@@ -755,10 +754,8 @@ fun TagLink(word: String, tags: List<List<String>>, canPreview: Boolean, backgro
var baseUserPair by remember { mutableStateOf<Pair<User, String?>?>(null) } var baseUserPair by remember { mutableStateOf<Pair<User, String?>?>(null) }
var baseNotePair by remember { mutableStateOf<Pair<Note, String?>?>(null) } var baseNotePair by remember { mutableStateOf<Pair<Note, String?>?>(null) }
val scope = rememberCoroutineScope()
LaunchedEffect(key1 = word) { LaunchedEffect(key1 = word) {
scope.launch(Dispatchers.IO) { launch(Dispatchers.IO) {
val matcher = tagIndex.matcher(word) val matcher = tagIndex.matcher(word)
val (index, suffix) = try { val (index, suffix) = try {
matcher.find() matcher.find()
@@ -262,9 +262,7 @@ fun AuthorGalleryZaps(
Column(modifier = Modifier.padding(start = 10.dp)) { Column(modifier = Modifier.padding(start = 10.dp)) {
FlowRow() { FlowRow() {
authorNotes.forEach { 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 authorPictureModifier = remember { Modifier }
val modifier = remember(comment) { val modifier = remember {
if (comment != null) { Modifier.clickable {
Modifier nav(route)
.fillMaxWidth()
.clickable {
nav(route)
}
} else {
Modifier.clickable {
nav(route)
}
} }
} }
@@ -381,7 +371,7 @@ private fun AuthorPictureAndComment(
content = it, content = it,
canPreview = true, canPreview = true,
tags = null, tags = null,
modifier = Modifier.weight(1f), modifier = remember { Modifier.fillMaxWidth() },
backgroundColor = backgroundColor, backgroundColor = backgroundColor,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav nav = nav
@@ -604,7 +604,7 @@ private fun RenderPoll(
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
val noteEvent = note.event as? PollNoteEvent ?: return val noteEvent = note.event as? PollNoteEvent ?: return
val eventContent = noteEvent.content() val eventContent = remember { noteEvent.content() }
if (makeItShort && accountViewModel.isLoggedUser(note.author)) { if (makeItShort && accountViewModel.isLoggedUser(note.author)) {
Text( Text(
@@ -2249,35 +2249,42 @@ fun UserPicture(
.align(Alignment.TopEnd) .align(Alignment.TopEnd)
} }
val myIconBackgroundModifier = remember { FollowingIcon(myIconBoxModifier, size)
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
)
}
} }
} }
} }
@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( data class DropDownParams(
val isFollowingAuthor: Boolean, val isFollowingAuthor: Boolean,
val isPrivateBookmarkNote: Boolean, val isPrivateBookmarkNote: Boolean,
@@ -4,14 +4,12 @@ import android.widget.Toast
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bolt import androidx.compose.material.icons.filled.Bolt
import androidx.compose.material.icons.outlined.Bolt import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip 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.res.stringResource
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight 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.text.style.TextAlign
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp 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.compose.ui.window.Popup
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note 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.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange 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 java.util.* import java.util.*
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -48,16 +43,11 @@ fun PollNote(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = remember(accountState) { accountState?.account } ?: return
val pollViewModel: PollNoteViewModel = viewModel( val pollViewModel: PollNoteViewModel = viewModel(
key = baseNote.idHex key = baseNote.idHex
) )
LaunchedEffect(key1 = baseNote) { pollViewModel.load(accountViewModel.account, baseNote)
pollViewModel.load(account, baseNote)
}
PollNote( PollNote(
baseNote = baseNote, baseNote = baseNote,
@@ -78,15 +68,11 @@ fun PollNote(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
val zapsState by baseNote.live().zaps.observeAsState() WatchZapsAndUpdateTallies(baseNote, pollViewModel)
LaunchedEffect(key1 = zapsState) { val tallies by pollViewModel.tallies.collectAsState()
withContext(Dispatchers.IO) {
pollViewModel.refreshTallies()
}
}
pollViewModel.tallies.forEach { poll_op -> tallies.forEach { poll_op ->
OptionNote( OptionNote(
poll_op, poll_op,
pollViewModel, 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 @Composable
private fun OptionNote( private fun OptionNote(
poolOption: PollOption, poolOption: PollOption,
@@ -109,6 +110,8 @@ private fun OptionNote(
backgroundColor: Color, backgroundColor: Color,
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
val tags = remember { baseNote.event?.tags() }
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 3.dp) modifier = Modifier.padding(vertical = 3.dp)
@@ -126,54 +129,16 @@ private fun OptionNote(
accountViewModel, accountViewModel,
pollViewModel, pollViewModel,
nonClickablePrepend = { nonClickablePrepend = {
Box( RenderOptionAfterVote(
Modifier poolOption.descriptor,
.fillMaxWidth(0.75f) poolOption.tally.toFloat(),
.clip(shape = RoundedCornerShape(15.dp)) color,
.border( canPreview,
2.dp, tags,
color, backgroundColor,
RoundedCornerShape(15.dp) accountViewModel,
) nav
) { )
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
)
}
}
}
}, },
clickablePrepend = { clickablePrepend = {
} }
@@ -186,32 +151,113 @@ private fun OptionNote(
pollViewModel, pollViewModel,
nonClickablePrepend = {}, nonClickablePrepend = {},
clickablePrepend = { clickablePrepend = {
Box( RenderOptionBeforeVote(poolOption.descriptor, canPreview, tags, backgroundColor, accountViewModel, nav)
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
)
}
} }
) )
} }
} }
} }
@Composable
private fun RenderOptionAfterVote(
description: String,
totalRatio: Float,
color: Color,
canPreview: Boolean,
tags: List<List<String>>?,
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<List<String>>?,
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 @Composable
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
fun ZapVote( fun ZapVote(
@@ -374,7 +420,7 @@ fun ZapVote(
} }
// only show tallies after a user has zapped note // only show tallies after a user has zapped note
if (baseNote.author == accountViewModel.userProfile() || pollViewModel.wasZappedByLoggedInAccount) { if (!pollViewModel.canZap()) {
Text( Text(
showAmount(poolOption.zappedValue), showAmount(poolOption.zappedValue),
fontSize = 14.sp, fontSize = 14.sp,
@@ -399,23 +445,18 @@ fun FilteredZapAmountChoicePopup(
val context = LocalContext.current val context = LocalContext.current
val accountState by accountViewModel.accountLiveData.observeAsState() 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 zapMessage = ""
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val options = account.zapAmountChoices.filter { pollViewModel.isValidInputVoteAmount(it) }.toMutableList() val sortedOptions = remember(accountState) {
if (options.isEmpty()) { pollViewModel.createZapOptionsThatMatchThePollingParameters()
pollViewModel.valueMinimum?.let { minimum ->
pollViewModel.valueMaximum?.let { maximum ->
if (minimum != maximum) {
options.add(((minimum + maximum) / 2).toLong())
}
}
}
} }
pollViewModel.valueMinimum?.let { options.add(it.toLong()) }
pollViewModel.valueMaximum?.let { options.add(it.toLong()) }
val sortedOptions = options.toSet().sorted()
Popup( Popup(
alignment = Alignment.BottomCenter, alignment = Alignment.BottomCenter,
@@ -424,6 +465,10 @@ fun FilteredZapAmountChoicePopup(
) { ) {
FlowRow(horizontalArrangement = Arrangement.Center) { FlowRow(horizontalArrangement = Arrangement.Center) {
sortedOptions.forEach { amountInSats -> sortedOptions.forEach { amountInSats ->
val zapAmount = remember {
"${showAmount(amountInSats.toBigDecimal().setScale(1))}"
}
Button( Button(
modifier = Modifier.padding(horizontal = 3.dp), modifier = Modifier.padding(horizontal = 3.dp),
onClick = { onClick = {
@@ -436,7 +481,7 @@ fun FilteredZapAmountChoicePopup(
context, context,
onError, onError,
onProgress, onProgress,
account.defaultZapType defaultZapType
) )
onDismiss() onDismiss()
} }
@@ -448,7 +493,7 @@ fun FilteredZapAmountChoicePopup(
) )
) { ) {
Text( Text(
"${showAmount(amountInSats.toBigDecimal().setScale(1))}", text = zapAmount,
color = Color.White, color = Color.White,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.combinedClickable( modifier = Modifier.combinedClickable(
@@ -462,7 +507,7 @@ fun FilteredZapAmountChoicePopup(
context, context,
onError, onError,
onProgress, onProgress,
account.defaultZapType defaultZapType
) )
onDismiss() 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 = {}
)
)
}
}
}
}
}
@@ -1,17 +1,23 @@
package com.vitorpamplona.amethyst.ui.note 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.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel 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.RelaySetupInfo
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.* import com.vitorpamplona.amethyst.service.model.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import java.math.BigDecimal import java.math.BigDecimal
import java.math.RoundingMode import java.math.RoundingMode
import java.util.* import java.util.*
@Immutable
data class PollOption( data class PollOption(
val option: Int, val option: Int,
val descriptor: String, val descriptor: String,
@@ -21,21 +27,23 @@ data class PollOption(
val zappedByLoggedIn: Boolean val zappedByLoggedIn: Boolean
) )
@Stable
class PollNoteViewModel : ViewModel() { class PollNoteViewModel : ViewModel() {
var account: Account? = null private var account: Account? = null
private var pollNote: Note? = null private var pollNote: Note? = null
var pollEvent: PollNoteEvent? = null private var pollEvent: PollNoteEvent? = null
var pollOptions: Map<Int, String>? = null private var pollOptions: Map<Int, String>? = null
var valueMaximum: Int? = null private var valueMaximum: Int? = null
var valueMinimum: Int? = null private var valueMinimum: Int? = null
private var closedAt: Int? = null private var closedAt: Int? = null
var consensusThreshold: BigDecimal? = null private var consensusThreshold: BigDecimal? = null
var totalZapped: BigDecimal = BigDecimal.ZERO private var totalZapped: BigDecimal = BigDecimal.ZERO
var wasZappedByLoggedInAccount: Boolean = false private var wasZappedByLoggedInAccount: Boolean = false
var tallies by mutableStateOf<List<PollOption>>(emptyList()) private val _tallies = MutableStateFlow<List<PollOption>>(emptyList())
val tallies = _tallies.asStateFlow()
fun load(acc: Account, note: Note?) { fun load(acc: Account, note: Note?) {
account = acc account = acc
@@ -46,36 +54,35 @@ class PollNoteViewModel : ViewModel() {
valueMinimum = pollEvent?.getTagInt(VALUE_MINIMUM) valueMinimum = pollEvent?.getTagInt(VALUE_MINIMUM)
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() { suspend fun refreshTallies() {
totalZapped = totalZapped() totalZapped = totalZapped()
wasZappedByLoggedInAccount = pollNote?.let { account?.calculateIfNoteWasZappedByAccount(it) } ?: false wasZappedByLoggedInAccount = pollNote?.let { account?.calculateIfNoteWasZappedByAccount(it) } ?: false
tallies = pollOptions?.keys?.map { _tallies.emit(
val zappedInOption = zappedPollOptionAmount(it) pollOptions?.keys?.map {
val zappedInOption = zappedPollOptionAmount(it)
val myTally = if (totalZapped.compareTo(BigDecimal.ZERO) > 0) { val myTally = if (totalZapped.compareTo(BigDecimal.ZERO) > 0) {
zappedInOption.divide(totalZapped, 2, RoundingMode.HALF_UP) zappedInOption.divide(totalZapped, 2, RoundingMode.HALF_UP)
} else { } else {
BigDecimal.ZERO 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) PollOption(it, pollOptions?.get(it) ?: "", zappedInOption, myTally, consensus, zappedByLoggedIn)
} ?: emptyList() } ?: emptyList()
)
} }
fun canZap(): Boolean { fun canZap(): Boolean {
val account = account ?: return false val account = account ?: return false
val user = account.userProfile() ?: return false
val note = pollNote ?: 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 fun isVoteAmountAtomic() = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum
@@ -154,4 +161,21 @@ class PollNoteViewModel : ViewModel() {
} }
} ?: BigDecimal(0) } ?: BigDecimal(0)
} }
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.toLong()) }
valueMaximum?.let { options.add(it.toLong()) }
return options.toSet().sorted()
}
} }
@@ -153,6 +153,10 @@ private fun FeedLoaded(
nav: (String) -> Unit, nav: (String) -> Unit,
routeForLastRead: String routeForLastRead: String
) { ) {
val defaultModifier = remember {
Modifier.fillMaxWidth().defaultMinSize(minHeight = 100.dp)
}
LazyColumn( LazyColumn(
modifier = remember { Modifier.fillMaxSize() }, modifier = remember { Modifier.fillMaxSize() },
contentPadding = remember { contentPadding = remember {
@@ -164,7 +168,7 @@ private fun FeedLoaded(
state = listState state = listState
) { ) {
itemsIndexed(state.feed.value, key = { _, item -> item.id() }) { _, item -> itemsIndexed(state.feed.value, key = { _, item -> item.id() }) { _, item ->
Row(Modifier.fillMaxWidth().defaultMinSize(minHeight = 100.dp)) { Row(defaultModifier) {
when (item) { when (item) {
is NoteCard -> NoteCompose( is NoteCard -> NoteCompose(
item.note, item.note,