kopia lustrzana https://github.com/vitorpamplona/amethyst
528 wiersze
17 KiB
Kotlin
528 wiersze
17 KiB
Kotlin
package com.vitorpamplona.amethyst.ui.note
|
|
|
|
import android.widget.Toast
|
|
import androidx.compose.foundation.*
|
|
import androidx.compose.foundation.layout.*
|
|
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.ui.Alignment
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.draw.clip
|
|
import androidx.compose.ui.graphics.Color
|
|
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.style.TextAlign
|
|
import androidx.compose.ui.unit.IntOffset
|
|
import androidx.compose.ui.unit.dp
|
|
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.ui.components.TranslatableRichTextViewer
|
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
|
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
|
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
|
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
|
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
|
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
|
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
|
import com.vitorpamplona.quartz.events.ImmutableListOfLists
|
|
import com.vitorpamplona.quartz.events.LnZapEvent
|
|
import com.vitorpamplona.quartz.events.toImmutableListOfLists
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.launch
|
|
import java.util.*
|
|
import kotlin.math.roundToInt
|
|
|
|
@Composable
|
|
fun PollNote(
|
|
baseNote: Note,
|
|
canPreview: Boolean,
|
|
backgroundColor: MutableState<Color>,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
val pollViewModel: PollNoteViewModel = viewModel(
|
|
key = baseNote.idHex + "PollNoteViewModel"
|
|
)
|
|
|
|
pollViewModel.load(accountViewModel.account, baseNote)
|
|
|
|
PollNote(
|
|
baseNote = baseNote,
|
|
pollViewModel = pollViewModel,
|
|
canPreview = canPreview,
|
|
backgroundColor = backgroundColor,
|
|
accountViewModel = accountViewModel,
|
|
nav = nav
|
|
)
|
|
}
|
|
|
|
@Composable
|
|
fun PollNote(
|
|
baseNote: Note,
|
|
pollViewModel: PollNoteViewModel,
|
|
canPreview: Boolean,
|
|
backgroundColor: MutableState<Color>,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
WatchZapsAndUpdateTallies(baseNote, pollViewModel)
|
|
|
|
val tallies by pollViewModel.tallies.collectAsState()
|
|
|
|
tallies.forEach { poll_op ->
|
|
OptionNote(
|
|
poll_op,
|
|
pollViewModel,
|
|
baseNote,
|
|
accountViewModel,
|
|
canPreview,
|
|
backgroundColor,
|
|
nav
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun WatchZapsAndUpdateTallies(
|
|
baseNote: Note,
|
|
pollViewModel: PollNoteViewModel
|
|
) {
|
|
val zapsState by baseNote.live().zaps.observeAsState()
|
|
|
|
LaunchedEffect(key1 = zapsState) {
|
|
launch(Dispatchers.Default) {
|
|
pollViewModel.refreshTallies()
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun OptionNote(
|
|
poolOption: PollOption,
|
|
pollViewModel: PollNoteViewModel,
|
|
baseNote: Note,
|
|
accountViewModel: AccountViewModel,
|
|
canPreview: Boolean,
|
|
backgroundColor: MutableState<Color>,
|
|
nav: (String) -> Unit
|
|
) {
|
|
val tags = remember(baseNote) {
|
|
baseNote.event?.tags()?.toImmutableListOfLists() ?: ImmutableListOfLists()
|
|
}
|
|
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
modifier = Modifier.padding(vertical = 3.dp)
|
|
) {
|
|
if (!pollViewModel.canZap()) {
|
|
val color = if (poolOption.consensusThreadhold) {
|
|
Color.Green.copy(alpha = 0.32f)
|
|
} else {
|
|
MaterialTheme.colors.mediumImportanceLink
|
|
}
|
|
|
|
ZapVote(
|
|
baseNote,
|
|
poolOption,
|
|
accountViewModel,
|
|
pollViewModel,
|
|
nonClickablePrepend = {
|
|
RenderOptionAfterVote(
|
|
poolOption.descriptor,
|
|
poolOption.tally.toFloat(),
|
|
color,
|
|
canPreview,
|
|
tags,
|
|
backgroundColor,
|
|
accountViewModel,
|
|
nav
|
|
)
|
|
},
|
|
clickablePrepend = {
|
|
}
|
|
)
|
|
} else {
|
|
ZapVote(
|
|
baseNote,
|
|
poolOption,
|
|
accountViewModel,
|
|
pollViewModel,
|
|
nonClickablePrepend = {},
|
|
clickablePrepend = {
|
|
RenderOptionBeforeVote(poolOption.descriptor, canPreview, tags, backgroundColor, accountViewModel, nav)
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun RenderOptionAfterVote(
|
|
description: String,
|
|
totalRatio: Float,
|
|
color: Color,
|
|
canPreview: Boolean,
|
|
tags: ImmutableListOfLists<String>,
|
|
backgroundColor: MutableState<Color>,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
val totalPercentage = remember(totalRatio) {
|
|
"${(totalRatio * 100).roundToInt()}%"
|
|
}
|
|
|
|
Box(
|
|
Modifier
|
|
.fillMaxWidth(0.75f)
|
|
.clip(shape = QuoteBorder)
|
|
.border(
|
|
2.dp,
|
|
color,
|
|
QuoteBorder
|
|
)
|
|
) {
|
|
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: ImmutableListOfLists<String>,
|
|
backgroundColor: MutableState<Color>,
|
|
accountViewModel: AccountViewModel,
|
|
nav: (String) -> Unit
|
|
) {
|
|
Box(
|
|
Modifier
|
|
.fillMaxWidth(0.75f)
|
|
.clip(shape = QuoteBorder)
|
|
.border(
|
|
2.dp,
|
|
MaterialTheme.colors.primary,
|
|
QuoteBorder
|
|
)
|
|
) {
|
|
TranslatableRichTextViewer(
|
|
description,
|
|
canPreview,
|
|
remember { Modifier.padding(15.dp) },
|
|
tags,
|
|
backgroundColor,
|
|
accountViewModel,
|
|
nav
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
@OptIn(ExperimentalFoundationApi::class)
|
|
fun ZapVote(
|
|
baseNote: Note,
|
|
poolOption: PollOption,
|
|
accountViewModel: AccountViewModel,
|
|
pollViewModel: PollNoteViewModel,
|
|
modifier: Modifier = Modifier,
|
|
nonClickablePrepend: @Composable () -> Unit,
|
|
clickablePrepend: @Composable () -> Unit
|
|
) {
|
|
val isLoggedUser by remember {
|
|
derivedStateOf {
|
|
accountViewModel.isLoggedUser(baseNote.author)
|
|
}
|
|
}
|
|
|
|
var wantsToZap by remember { mutableStateOf(false) }
|
|
var zappingProgress by remember { mutableStateOf(0f) }
|
|
|
|
val context = LocalContext.current
|
|
val scope = rememberCoroutineScope()
|
|
|
|
nonClickablePrepend()
|
|
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
modifier = Modifier.combinedClickable(
|
|
role = Role.Button,
|
|
// interactionSource = remember { MutableInteractionSource() },
|
|
// indication = rememberRipple(bounded = false, radius = 24.dp),
|
|
onClick = {
|
|
if (!accountViewModel.isWriteable()) {
|
|
scope.launch {
|
|
Toast
|
|
.makeText(
|
|
context,
|
|
context.getString(R.string.login_with_a_private_key_to_be_able_to_send_zaps),
|
|
Toast.LENGTH_SHORT
|
|
)
|
|
.show()
|
|
}
|
|
} else if (pollViewModel.isPollClosed()) {
|
|
scope.launch {
|
|
Toast
|
|
.makeText(
|
|
context,
|
|
context.getString(R.string.poll_is_closed),
|
|
Toast.LENGTH_SHORT
|
|
)
|
|
.show()
|
|
}
|
|
} else if (isLoggedUser) {
|
|
scope.launch {
|
|
Toast
|
|
.makeText(
|
|
context,
|
|
context.getString(R.string.poll_author_no_vote),
|
|
Toast.LENGTH_SHORT
|
|
)
|
|
.show()
|
|
}
|
|
} else if (pollViewModel.isVoteAmountAtomic() && poolOption.zappedByLoggedIn) {
|
|
// only allow one vote per option when min==max, i.e. atomic vote amount specified
|
|
scope.launch {
|
|
Toast
|
|
.makeText(
|
|
context,
|
|
R.string.one_vote_per_user_on_atomic_votes,
|
|
Toast.LENGTH_SHORT
|
|
)
|
|
.show()
|
|
}
|
|
return@combinedClickable
|
|
} else if (accountViewModel.account.zapAmountChoices.size == 1 &&
|
|
pollViewModel.isValidInputVoteAmount(accountViewModel.account.zapAmountChoices.first())
|
|
) {
|
|
accountViewModel.zap(
|
|
baseNote,
|
|
accountViewModel.account.zapAmountChoices.first() * 1000,
|
|
poolOption.option,
|
|
"",
|
|
context,
|
|
onError = {
|
|
scope.launch {
|
|
zappingProgress = 0f
|
|
Toast
|
|
.makeText(context, it, Toast.LENGTH_SHORT)
|
|
.show()
|
|
}
|
|
},
|
|
onProgress = {
|
|
scope.launch(Dispatchers.Main) {
|
|
zappingProgress = it
|
|
}
|
|
},
|
|
zapType = accountViewModel.account.defaultZapType
|
|
)
|
|
} else {
|
|
wantsToZap = true
|
|
}
|
|
}
|
|
)
|
|
) {
|
|
if (wantsToZap) {
|
|
FilteredZapAmountChoicePopup(
|
|
baseNote,
|
|
accountViewModel,
|
|
pollViewModel,
|
|
poolOption.option,
|
|
onDismiss = {
|
|
wantsToZap = false
|
|
zappingProgress = 0f
|
|
},
|
|
onChangeAmount = {
|
|
wantsToZap = false
|
|
},
|
|
onError = {
|
|
scope.launch {
|
|
zappingProgress = 0f
|
|
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
|
|
}
|
|
},
|
|
onProgress = {
|
|
scope.launch(Dispatchers.Main) {
|
|
zappingProgress = it
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
clickablePrepend()
|
|
|
|
if (poolOption.zappedByLoggedIn) {
|
|
zappingProgress = 1f
|
|
Icon(
|
|
imageVector = Icons.Default.Bolt,
|
|
contentDescription = stringResource(R.string.zaps),
|
|
modifier = Modifier.size(20.dp),
|
|
tint = BitcoinOrange
|
|
)
|
|
} else {
|
|
if (zappingProgress < 0.1 || zappingProgress > 0.99) {
|
|
Icon(
|
|
imageVector = Icons.Outlined.Bolt,
|
|
contentDescription = stringResource(id = R.string.zaps),
|
|
modifier = Modifier.size(20.dp),
|
|
tint = MaterialTheme.colors.placeholderText
|
|
)
|
|
} else {
|
|
Spacer(Modifier.width(3.dp))
|
|
CircularProgressIndicator(
|
|
progress = zappingProgress,
|
|
modifier = Modifier.size(14.dp),
|
|
strokeWidth = 2.dp
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// only show tallies after a user has zapped note
|
|
if (!pollViewModel.canZap()) {
|
|
val amountStr = remember(poolOption.zappedValue) {
|
|
showAmount(poolOption.zappedValue)
|
|
}
|
|
Text(
|
|
text = amountStr,
|
|
fontSize = Font14SP,
|
|
color = MaterialTheme.colors.placeholderText,
|
|
modifier = modifier
|
|
)
|
|
}
|
|
}
|
|
|
|
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
|
|
@Composable
|
|
fun FilteredZapAmountChoicePopup(
|
|
baseNote: Note,
|
|
accountViewModel: AccountViewModel,
|
|
pollViewModel: PollNoteViewModel,
|
|
pollOption: Int,
|
|
onDismiss: () -> Unit,
|
|
onChangeAmount: () -> Unit,
|
|
onError: (text: String) -> Unit,
|
|
onProgress: (percent: Float) -> Unit
|
|
) {
|
|
val context = LocalContext.current
|
|
|
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
|
val defaultZapType by remember(accountState) {
|
|
derivedStateOf {
|
|
accountState?.account?.defaultZapType ?: LnZapEvent.ZapType.PRIVATE
|
|
}
|
|
}
|
|
|
|
val zapMessage = ""
|
|
|
|
val sortedOptions = remember(accountState) {
|
|
pollViewModel.createZapOptionsThatMatchThePollingParameters()
|
|
}
|
|
|
|
Popup(
|
|
alignment = Alignment.BottomCenter,
|
|
offset = IntOffset(0, -100),
|
|
onDismissRequest = { onDismiss() }
|
|
) {
|
|
FlowRow(horizontalArrangement = Arrangement.Center) {
|
|
sortedOptions.forEach { amountInSats ->
|
|
val zapAmount = remember {
|
|
"⚡ ${showAmount(amountInSats.toBigDecimal().setScale(1))}"
|
|
}
|
|
|
|
Button(
|
|
modifier = Modifier.padding(horizontal = 3.dp),
|
|
onClick = {
|
|
accountViewModel.zap(
|
|
baseNote,
|
|
amountInSats * 1000,
|
|
pollOption,
|
|
zapMessage,
|
|
context,
|
|
onError,
|
|
onProgress,
|
|
defaultZapType
|
|
)
|
|
onDismiss()
|
|
},
|
|
shape = ButtonBorder,
|
|
colors = ButtonDefaults
|
|
.buttonColors(
|
|
backgroundColor = MaterialTheme.colors.primary
|
|
)
|
|
) {
|
|
Text(
|
|
text = zapAmount,
|
|
color = Color.White,
|
|
textAlign = TextAlign.Center,
|
|
modifier = Modifier.combinedClickable(
|
|
onClick = {
|
|
accountViewModel.zap(
|
|
baseNote,
|
|
amountInSats * 1000,
|
|
pollOption,
|
|
zapMessage,
|
|
context,
|
|
onError,
|
|
onProgress,
|
|
defaultZapType
|
|
)
|
|
onDismiss()
|
|
},
|
|
onLongClick = {
|
|
onChangeAmount()
|
|
}
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|