Merge pull request #330 from vitorpamplona/pr/321

Moving new pool button to NewPost
This commit is contained in:
Vitor Pamplona
2023-04-03 20:35:41 -04:00
committed by GitHub
16 changed files with 459 additions and 290 deletions
-4
View File
@@ -31,10 +31,6 @@ android {
applicationIdSuffix '.debug'
versionNameSuffix '-DEBUG'
resValue "string", "app_name", "@string/app_name_debug"
lintOptions{
disable 'MissingTranslation'
}
}
}
@@ -21,10 +21,10 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.NewPollViewModel
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
@Composable
fun NewPollClosing(pollViewModel: NewPollViewModel) {
fun NewPollClosing(pollViewModel: NewPostViewModel) {
var text by rememberSaveable { mutableStateOf("") }
pollViewModel.isValidClosedAt.value = true
@@ -75,5 +75,5 @@ fun NewPollClosing(pollViewModel: NewPollViewModel) {
@Preview
@Composable
fun NewPollClosingPreview() {
NewPollClosing(NewPollViewModel())
NewPollClosing(NewPostViewModel())
}
@@ -21,10 +21,10 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.NewPollViewModel
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
@Composable
fun NewPollConsensusThreshold(pollViewModel: NewPollViewModel) {
fun NewPollConsensusThreshold(pollViewModel: NewPostViewModel) {
var text by rememberSaveable { mutableStateOf("") }
pollViewModel.isValidConsensusThreshold.value = true
@@ -75,5 +75,5 @@ fun NewPollConsensusThreshold(pollViewModel: NewPollViewModel) {
@Preview
@Composable
fun NewPollConsensusThresholdPreview() {
NewPollConsensusThreshold(NewPollViewModel())
NewPollConsensusThreshold(NewPostViewModel())
}
@@ -1,20 +1,20 @@
package com.vitorpamplona.amethyst.ui.actions
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
@Composable
fun NewPollOption(pollViewModel: NewPollViewModel, optionIndex: Int) {
fun NewPollOption(pollViewModel: NewPostViewModel, optionIndex: Int) {
val colorInValid = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = MaterialTheme.colors.error,
unfocusedBorderColor = Color.Red
@@ -25,14 +25,26 @@ fun NewPollOption(pollViewModel: NewPollViewModel, optionIndex: Int) {
)
Row {
val deleteIcon: @Composable (() -> Unit) = {
IconButton(
onClick = {
pollViewModel.pollOptions.remove(optionIndex)
}
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = stringResource(R.string.clear)
)
}
}
OutlinedTextField(
modifier = Modifier
.weight(1F),
modifier = Modifier.weight(1F),
value = pollViewModel.pollOptions[optionIndex] ?: "",
onValueChange = { pollViewModel.pollOptions[optionIndex] = it },
label = {
Text(
text = stringResource(R.string.poll_option_index).format(optionIndex),
text = stringResource(R.string.poll_option_index).format(optionIndex + 1),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
},
@@ -42,31 +54,17 @@ fun NewPollOption(pollViewModel: NewPollViewModel, optionIndex: Int) {
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
},
colors = if (pollViewModel.pollOptions[optionIndex]?.isNotEmpty() == true) colorValid else colorInValid
keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences
),
// colors = if (pollViewModel.pollOptions[optionIndex]?.isNotEmpty() == true) colorValid else colorInValid,
trailingIcon = if (optionIndex > 1) deleteIcon else null
)
if (optionIndex > 1) {
Button(
modifier = Modifier
.padding(start = 6.dp, top = 2.dp)
.imePadding(),
onClick = { pollViewModel.pollOptions.remove(optionIndex) },
border = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f)),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
) {
Image(
painterResource(id = android.R.drawable.ic_delete),
contentDescription = "Remove poll option button",
modifier = Modifier.size(18.dp)
)
}
}
}
}
@Preview
@Composable
fun NewPollOptionPreview() {
NewPollOption(NewPollViewModel(), 0)
NewPollOption(NewPostViewModel(), 0)
}
@@ -17,12 +17,12 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.NewPollViewModel
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun NewPollPrimaryDescription(pollViewModel: NewPollViewModel) {
fun NewPollPrimaryDescription(pollViewModel: NewPostViewModel) {
// initialize focus reference to be able to request focus programmatically
val focusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
@@ -9,10 +9,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.actions.NewPollViewModel
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
@Composable
fun NewPollRecipientsField(pollViewModel: NewPollViewModel, account: Account) {
fun NewPollRecipientsField(pollViewModel: NewPostViewModel, account: Account) {
// if no recipients, add user's pubkey
if (pollViewModel.zapRecipients.isEmpty()) {
pollViewModel.zapRecipients.add(account.userProfile().pubkeyHex)
@@ -33,7 +33,7 @@ import kotlinx.coroutines.delay
@Composable
fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = null, account: Account) {
val pollViewModel: NewPollViewModel = viewModel()
val pollViewModel: NewPostViewModel = viewModel()
val context = LocalContext.current
@@ -84,7 +84,7 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
PollButton(
onPost = {
pollViewModel.sendPoll()
pollViewModel.sendPost()
onClose()
},
isActive = pollViewModel.message.text.isNotBlank() &&
@@ -1,141 +0,0 @@
package com.vitorpamplona.amethyst.ui.actions
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.text.input.TextFieldValue
import com.vitorpamplona.amethyst.model.*
import com.vitorpamplona.amethyst.service.nip19.Nip19
class NewPollViewModel : NewPostViewModel() {
var zapRecipients = mutableStateListOf<HexKey>()
var pollOptions = newStateMapPollOptions()
var valueMaximum: Int? = null
var valueMinimum: Int? = null
var consensusThreshold: Int? = null
var closedAt: Int? = null
var isValidRecipients = mutableStateOf(true)
var isValidvalueMaximum = mutableStateOf(true)
var isValidvalueMinimum = mutableStateOf(true)
var isValidConsensusThreshold = mutableStateOf(true)
var isValidClosedAt = mutableStateOf(true)
override fun load(account: Account, replyingTo: Note?, quote: Note?) {
super.load(account, replyingTo, quote)
}
override fun addUserToMentions(user: User) {
super.addUserToMentions(user)
}
override fun addNoteToReplyTos(note: Note) {
super.addNoteToReplyTos(note)
}
override fun tagIndex(user: User): Int {
return super.tagIndex(user)
}
override fun tagIndex(note: Note): Int {
return super.tagIndex(note)
}
fun sendPoll() {
// adds all references to mentions and reply tos
message.text.split('\n').forEach { paragraph: String ->
paragraph.split(' ').forEach { word: String ->
val results = parseDirtyWordForKey(word)
if (results?.key?.type == Nip19.Type.USER) {
addUserToMentions(LocalCache.getOrCreateUser(results.key.hex))
} else if (results?.key?.type == Nip19.Type.NOTE) {
addNoteToReplyTos(LocalCache.getOrCreateNote(results.key.hex))
} else if (results?.key?.type == Nip19.Type.ADDRESS) {
val note = LocalCache.checkGetOrCreateAddressableNote(results.key.hex)
if (note != null) {
addNoteToReplyTos(note)
}
}
}
}
// Tags the text in the correct order.
val newMessage = message.text.split('\n').map { paragraph: String ->
paragraph.split(' ').map { word: String ->
val results = parseDirtyWordForKey(word)
if (results?.key?.type == Nip19.Type.USER) {
val user = LocalCache.getOrCreateUser(results.key.hex)
"#[${tagIndex(user)}]${results.restOfWord}"
} else if (results?.key?.type == Nip19.Type.NOTE) {
val note = LocalCache.getOrCreateNote(results.key.hex)
"#[${tagIndex(note)}]${results.restOfWord}"
} else if (results?.key?.type == Nip19.Type.ADDRESS) {
val note = LocalCache.checkGetOrCreateAddressableNote(results.key.hex)
if (note != null) {
"#[${tagIndex(note)}]${results.restOfWord}"
} else {
word
}
} else {
word
}
}.joinToString(" ")
}.joinToString("\n")
/* if (originalNote?.channel() != null) {
account?.sendChannelMessage(newMessage, originalNote!!.channel()!!.idHex, originalNote!!, mentions)
} else {
account?.sendPoll(newMessage, replyTos, mentions)
}*/
account?.sendPoll(newMessage, replyTos, mentions, pollOptions, valueMaximum, valueMinimum, consensusThreshold, closedAt)
clearPollStates()
}
override fun cancel() {
super.cancel()
clearPollStates()
}
override fun findUrlInMessage(): String? {
return super.findUrlInMessage()
}
override fun removeFromReplyList(it: User) {
super.removeFromReplyList(it)
}
override fun updateMessage(it: TextFieldValue) {
super.updateMessage(it)
}
override fun autocompleteWithUser(item: User) {
super.autocompleteWithUser(item)
}
// clear all states
private fun clearPollStates() {
message = TextFieldValue("")
urlPreview = null
isUploadingImage = false
mentions = null
zapRecipients = mutableStateListOf<HexKey>()
pollOptions = newStateMapPollOptions()
valueMaximum = null
valueMinimum = null
consensusThreshold = null
closedAt = null
}
private fun newStateMapPollOptions(): SnapshotStateMap<Int, String> {
return mutableStateMapOf(Pair(0, ""), Pair(1, ""))
}
}
@@ -23,7 +23,7 @@ import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
@Composable
fun NewPollVoteValueRange(pollViewModel: NewPollViewModel) {
fun NewPollVoteValueRange(pollViewModel: NewPostViewModel) {
var textMax by rememberSaveable { mutableStateOf("") }
var textMin by rememberSaveable { mutableStateOf("") }
@@ -122,5 +122,5 @@ fun NewPollVoteValueRange(pollViewModel: NewPollViewModel) {
@Preview
@Composable
fun NewPollVoteValueRangePreview() {
NewPollVoteValueRange(NewPollViewModel())
NewPollVoteValueRange(NewPostViewModel())
}
@@ -1,6 +1,8 @@
package com.vitorpamplona.amethyst.ui.actions
import android.widget.Toast
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
@@ -104,8 +106,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
postViewModel.sendPost()
onClose()
},
isActive = postViewModel.message.text.isNotBlank() &&
!postViewModel.isUploadingImage
isActive = postViewModel.canPost()
)
}
@@ -161,6 +162,26 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
if (postViewModel.wantsPoll) {
postViewModel.pollOptions.values.forEachIndexed { index, element ->
NewPollOption(postViewModel, index)
}
Button(
onClick = { postViewModel.pollOptions[postViewModel.pollOptions.size] = "" },
border = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f)),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
) {
Image(
painterResource(id = android.R.drawable.ic_input_add),
contentDescription = "Add poll option button",
modifier = Modifier.size(18.dp)
)
}
}
val myUrlPreview = postViewModel.urlPreview
if (myUrlPreview != null) {
Row(modifier = Modifier.padding(top = 5.dp)) {
@@ -217,11 +238,15 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
Row(modifier = Modifier.fillMaxWidth()) {
UploadFromGallery(
isUploading = postViewModel.isUploadingImage,
tint = MaterialTheme.colors.primary,
tint = MaterialTheme.colors.onBackground,
modifier = Modifier.padding(bottom = 10.dp)
) {
postViewModel.upload(it, context)
}
AddPollButton(postViewModel.wantsPoll) {
postViewModel.wantsPoll = !postViewModel.wantsPoll
}
}
}
}
@@ -229,6 +254,34 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
}
}
@Composable
private fun AddPollButton(
isPollActive: Boolean,
onClick: () -> Unit
) {
IconButton(
onClick = {
onClick()
}
) {
if (!isPollActive) {
Icon(
painter = painterResource(R.drawable.ic_poll),
null,
modifier = Modifier.size(20.dp),
tint = Color.White
)
} else {
Icon(
painter = painterResource(R.drawable.ic_lists),
null,
modifier = Modifier.size(20.dp),
tint = Color.White
)
}
}
}
@Composable
fun CloseButton(onCancel: () -> Unit) {
Button(
@@ -3,8 +3,11 @@ package com.vitorpamplona.amethyst.ui.actions
import android.content.Context
import android.net.Uri
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel
@@ -34,6 +37,21 @@ open class NewPostViewModel : ViewModel() {
var userSuggestions by mutableStateOf<List<User>>(emptyList())
var userSuggestionAnchor: TextRange? = null
// Polls
var wantsPoll by mutableStateOf(false)
var zapRecipients = mutableStateListOf<HexKey>()
var pollOptions = newStateMapPollOptions()
var valueMaximum: Int? = null
var valueMinimum: Int? = null
var consensusThreshold: Int? = null
var closedAt: Int? = null
var isValidRecipients = mutableStateOf(true)
var isValidvalueMaximum = mutableStateOf(true)
var isValidvalueMinimum = mutableStateOf(true)
var isValidConsensusThreshold = mutableStateOf(true)
var isValidClosedAt = mutableStateOf(true)
open fun load(account: Account, replyingTo: Note?, quote: Note?) {
originalNote = replyingTo
replyingTo?.let { replyNote ->
@@ -130,16 +148,15 @@ open class NewPostViewModel : ViewModel() {
}.joinToString(" ")
}.joinToString("\n")
if (originalNote?.channel() != null) {
if (wantsPoll) {
account?.sendPoll(newMessage, replyTos, mentions, pollOptions, valueMaximum, valueMinimum, consensusThreshold, closedAt)
} else if (originalNote?.channel() != null) {
account?.sendChannelMessage(newMessage, originalNote!!.channel()!!.idHex, originalNote!!, mentions)
} else {
account?.sendPost(newMessage, replyTos, mentions)
}
message = TextFieldValue("")
urlPreview = null
isUploadingImage = false
mentions = null
cancel()
}
fun upload(it: Uri, context: Context) {
@@ -171,6 +188,14 @@ open class NewPostViewModel : ViewModel() {
urlPreview = null
isUploadingImage = false
mentions = null
wantsPoll = false
zapRecipients = mutableStateListOf<HexKey>()
pollOptions = newStateMapPollOptions()
valueMaximum = null
valueMinimum = null
consensusThreshold = null
closedAt = null
}
open fun findUrlInMessage(): String? {
@@ -214,4 +239,13 @@ open class NewPostViewModel : ViewModel() {
userSuggestions = emptyList()
}
}
private fun newStateMapPollOptions(): SnapshotStateMap<Int, String> {
return mutableStateMapOf(Pair(0, ""), Pair(1, ""))
}
fun canPost(): Boolean {
return message.text.isNotBlank() && !isUploadingImage &&
(!wantsPoll || pollOptions.values.all { it.isNotEmpty() })
}
}
@@ -0,0 +1,47 @@
package com.vitorpamplona.amethyst.ui.buttons
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.actions.NewPostView
@Composable
fun NewNoteButton(account: Account) {
var wantsToPost by remember {
mutableStateOf(false)
}
if (wantsToPost) {
NewPostView({ wantsToPost = false }, account = account)
}
OutlinedButton(
onClick = { wantsToPost = true },
modifier = Modifier.size(55.dp),
shape = CircleShape,
colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.primary),
contentPadding = PaddingValues(0.dp)
) {
Icon(
painter = painterResource(R.drawable.ic_compose),
null,
modifier = Modifier.size(26.dp),
tint = Color.White
)
}
}
@@ -524,17 +524,6 @@ fun NoteComposeInner(
DisplayUncitedHashtags(noteEvent, eventContent, navController)
}
/*
TranslateableRichTextViewer(
eventContent,
canPreview = canPreview && !makeItShort,
Modifier.fillMaxWidth(),
noteEvent.tags(),
backgroundColor,
accountViewModel,
navController
)
*/
if (noteEvent is PollNoteEvent) {
PollNote(
@@ -16,16 +16,20 @@ 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
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.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.navigation.NavController
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
@@ -35,55 +39,119 @@ import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.*
import kotlin.math.roundToInt
@Composable
fun PollNote(
note: Note,
baseNote: Note,
canPreview: Boolean,
backgroundColor: Color,
accountViewModel: AccountViewModel,
navController: NavController
) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
val zapsState by baseNote.live().zaps.observeAsState()
val zappedNote = zapsState?.note ?: return
val pollViewModel = PollNoteViewModel()
pollViewModel.load(note)
pollViewModel.load(zappedNote)
pollViewModel.pollEvent?.pollOptions()?.forEach { poll_op ->
val optionTally = pollViewModel.optionVoteTally(poll_op.key)
Row(
verticalAlignment = Alignment.CenterVertically
val color = if (
pollViewModel.consensusThreshold != null &&
optionTally >= pollViewModel.consensusThreshold!!
) {
TranslatableRichTextViewer(
poll_op.value,
canPreview,
modifier = Modifier
.width(250.dp)
.padding(0.dp) // padding outside border
.border(BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f)))
.padding(4.dp), // padding between border and text
pollViewModel.pollEvent?.tags(),
backgroundColor,
accountViewModel,
navController
)
ZapVote(note, accountViewModel, pollViewModel, poll_op.key)
Color.Green.copy(alpha = 0.32f)
} else {
MaterialTheme.colors.primary.copy(alpha = 0.32f)
}
Row(
verticalAlignment = Alignment.CenterVertically
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 3.dp)
) {
// only show tallies after user has zapped note
if (note.isZappedBy(accountViewModel.userProfile())) {
LinearProgressIndicator(
modifier = Modifier.width(250.dp),
color = if (
pollViewModel.consensusThreshold != null &&
optionTally >= pollViewModel.consensusThreshold!!
) {
Color.Green
} else { MaterialTheme.colors.primary },
progress = optionTally
if (zappedNote.author == account.userProfile() || zappedNote.isZappedBy(account.userProfile()) == true) {
ZapVote(
baseNote,
accountViewModel,
pollViewModel,
poll_op.key,
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 = optionTally
)
Row(
verticalAlignment = Alignment.CenterVertically
) {
Column(
horizontalAlignment = Alignment.End,
modifier = Modifier.padding(horizontal = 10.dp).width(40.dp)
) {
Text(
text = "${(optionTally * 100).roundToInt()}%",
fontWeight = FontWeight.Bold
)
}
Column(modifier = Modifier.fillMaxWidth()) {
TranslatableRichTextViewer(
poll_op.value,
canPreview,
Modifier.padding(vertical = 5.dp),
pollViewModel.pollEvent?.tags(),
backgroundColor,
accountViewModel,
navController
)
}
}
}
},
clickablePrepend = {
}
)
} else {
ZapVote(
baseNote,
accountViewModel,
pollViewModel,
poll_op.key,
nonClickablePrepend = {},
clickablePrepend = {
Box(
Modifier.fillMaxWidth(0.75f)
.clip(shape = RoundedCornerShape(15.dp))
.border(
2.dp,
MaterialTheme.colors.primary,
RoundedCornerShape(15.dp)
)
) {
TranslatableRichTextViewer(
poll_op.value,
canPreview,
Modifier.padding(vertical = 5.dp, horizontal = 10.dp),
pollViewModel.pollEvent?.tags(),
backgroundColor,
accountViewModel,
navController
)
}
}
)
}
}
@@ -97,11 +165,10 @@ fun ZapVote(
accountViewModel: AccountViewModel,
pollViewModel: PollNoteViewModel,
pollOption: Int,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
nonClickablePrepend: @Composable () -> Unit,
clickablePrepend: @Composable () -> Unit
) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
val zapsState by baseNote.live().zaps.observeAsState()
val zappedNote = zapsState?.note
@@ -112,63 +179,95 @@ fun ZapVote(
var zappingProgress by remember { mutableStateOf(0f) }
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
nonClickablePrepend()
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.then(Modifier.size(20.dp))
.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 (pollViewModel.isVoteAmountAtomic()) {
// only allow one vote per option when min==max, i.e. atomic vote amount specified
if (pollViewModel.isPollOptionZappedBy(pollOption, account.userProfile())) {
scope.launch {
Toast
.makeText(context, R.string.one_vote_per_user_on_atomic_votes, Toast.LENGTH_SHORT)
.show()
}
return@combinedClickable
} else { wantsToZap = true }
} else {
wantsToZap = true
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()
}
},
onLongClick = {}
)
} else if (pollViewModel.isPollClosed()) {
scope.launch {
Toast
.makeText(
context,
context.getString(R.string.poll_is_closed),
Toast.LENGTH_SHORT
)
.show()
}
} else if (pollViewModel.isVoteAmountAtomic() && pollViewModel.isPollOptionZappedBy(pollOption, accountViewModel.userProfile())) {
// 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 (account.zapAmountChoices.size == 1 && pollViewModel.isValidInputVoteAmount(account.zapAmountChoices.first())) {
scope.launch(Dispatchers.IO) {
accountViewModel.zap(
baseNote,
account.zapAmountChoices.first() * 1000,
pollOption,
"",
context,
onError = {
scope.launch {
zappingProgress = 0f
Toast
.makeText(context, it, Toast.LENGTH_SHORT)
.show()
}
},
onProgress = {
scope.launch(Dispatchers.Main) {
zappingProgress = it
}
}
)
}
} else {
wantsToZap = true
}
}
)
) {
if (wantsToZap) {
ZapVoteAmountChoicePopup(
FilteredZapAmountChoicePopup(
baseNote,
accountViewModel,
pollViewModel,
pollOption,
onDismiss = {
wantsToZap = false
zappingProgress = 0f
},
onChangeAmount = {
wantsToZap = false
},
onError = {
scope.launch {
zappingProgress = 0f
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
}
},
@@ -180,7 +279,9 @@ fun ZapVote(
)
}
if (pollViewModel.isPollOptionZappedBy(pollOption, account.userProfile())) {
clickablePrepend()
if (pollViewModel.isPollOptionZappedBy(pollOption, accountViewModel.userProfile())) {
Icon(
imageVector = Icons.Default.Bolt,
contentDescription = stringResource(R.string.zaps),
@@ -198,7 +299,7 @@ fun ZapVote(
}
// only show tallies after a user has zapped note
if (zappedNote?.isZappedBy(account.userProfile()) == true) {
if (baseNote.author == accountViewModel.userProfile() || zappedNote?.isZappedBy(accountViewModel.userProfile()) == true) {
Text(
showAmount(pollViewModel.zappedPollOptionAmount(pollOption)),
fontSize = 14.sp,
@@ -208,6 +309,98 @@ fun ZapVote(
}
}
@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 account = accountState?.account ?: return
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())
}
}
}
}
pollViewModel.valueMinimum?.let { options.add(it.toLong()) }
pollViewModel.valueMaximum?.let { options.add(it.toLong()) }
val sortedOptions = options.toSet().sorted()
Popup(
alignment = Alignment.BottomCenter,
offset = IntOffset(0, -100),
onDismissRequest = { onDismiss() }
) {
FlowRow(horizontalArrangement = Arrangement.Center) {
sortedOptions.forEach { amountInSats ->
Button(
modifier = Modifier.padding(horizontal = 3.dp),
onClick = {
scope.launch(Dispatchers.IO) {
accountViewModel.zap(
baseNote,
amountInSats * 1000,
pollOption,
zapMessage,
context,
onError,
onProgress
)
onDismiss()
}
},
shape = RoundedCornerShape(20.dp),
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.primary
)
) {
Text(
"${showAmount(amountInSats.toBigDecimal().setScale(1))}",
color = Color.White,
textAlign = TextAlign.Center,
modifier = Modifier.combinedClickable(
onClick = {
scope.launch(Dispatchers.IO) {
accountViewModel.zap(
baseNote,
amountInSats * 1000,
pollOption,
zapMessage,
context,
onError,
onProgress
)
onDismiss()
}
},
onLongClick = {
onChangeAmount()
}
)
)
}
}
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ZapVoteAmountChoicePopup(
@@ -14,7 +14,7 @@ class PollNoteViewModel {
var pollEvent: PollNoteEvent? = null
private var pollOptions: Map<Int, String>? = null
var valueMaximum: Int? = null
private var valueMinimum: Int? = null
var valueMinimum: Int? = null
private var closedAt: Int? = null
var consensusThreshold: Float? = null
@@ -23,7 +23,7 @@ import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import com.vitorpamplona.amethyst.buttons.NewChannelButton
import com.vitorpamplona.amethyst.ui.buttons.FabColumn
import com.vitorpamplona.amethyst.ui.buttons.NewNoteButton
import com.vitorpamplona.amethyst.ui.navigation.*
import com.vitorpamplona.amethyst.ui.navigation.AccountSwitchBottomSheet
import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar
@@ -91,7 +91,7 @@ fun FloatingButtons(navController: NavHostController, accountViewModel: AccountS
// Does nothing.
}
is AccountState.LoggedIn -> {
FabColumn(state.account)
NewNoteButton(state.account)
}
}
}