Improves Poll Rendering speed.
This commit is contained in:
@@ -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<Note?>(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<User?>(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<Nip19.Return?>(null) }
|
||||
var baseNotePair by remember { mutableStateOf<Pair<Note, String?>?>(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<Pair<String, String?>?>(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<List<String>>, canPreview: Boolean, backgro
|
||||
var baseUserPair by remember { mutableStateOf<Pair<User, String?>?>(null) }
|
||||
var baseNotePair by remember { mutableStateOf<Pair<Note, String?>?>(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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<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
|
||||
@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 = {}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Int, String>? = null
|
||||
var valueMaximum: Int? = null
|
||||
var valueMinimum: Int? = null
|
||||
private var pollEvent: PollNoteEvent? = null
|
||||
private var pollOptions: Map<Int, String>? = 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<List<PollOption>>(emptyList())
|
||||
private val _tallies = MutableStateFlow<List<PollOption>>(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<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,
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user