Moving new pool button to NewPost

This commit is contained in:
Vitor Pamplona
2023-04-03 20:31:25 -04:00
parent 9d9283e05f
commit 6a85ab743d
16 changed files with 459 additions and 290 deletions
-4
View File
@@ -31,10 +31,6 @@ android {
applicationIdSuffix '.debug' applicationIdSuffix '.debug'
versionNameSuffix '-DEBUG' versionNameSuffix '-DEBUG'
resValue "string", "app_name", "@string/app_name_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.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.NewPollViewModel import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
@Composable @Composable
fun NewPollClosing(pollViewModel: NewPollViewModel) { fun NewPollClosing(pollViewModel: NewPostViewModel) {
var text by rememberSaveable { mutableStateOf("") } var text by rememberSaveable { mutableStateOf("") }
pollViewModel.isValidClosedAt.value = true pollViewModel.isValidClosedAt.value = true
@@ -75,5 +75,5 @@ fun NewPollClosing(pollViewModel: NewPollViewModel) {
@Preview @Preview
@Composable @Composable
fun NewPollClosingPreview() { 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.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.NewPollViewModel import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
@Composable @Composable
fun NewPollConsensusThreshold(pollViewModel: NewPollViewModel) { fun NewPollConsensusThreshold(pollViewModel: NewPostViewModel) {
var text by rememberSaveable { mutableStateOf("") } var text by rememberSaveable { mutableStateOf("") }
pollViewModel.isValidConsensusThreshold.value = true pollViewModel.isValidConsensusThreshold.value = true
@@ -75,5 +75,5 @@ fun NewPollConsensusThreshold(pollViewModel: NewPollViewModel) {
@Preview @Preview
@Composable @Composable
fun NewPollConsensusThresholdPreview() { fun NewPollConsensusThresholdPreview() {
NewPollConsensusThreshold(NewPollViewModel()) NewPollConsensusThreshold(NewPostViewModel())
} }
@@ -1,20 +1,20 @@
package com.vitorpamplona.amethyst.ui.actions 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.layout.*
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.filled.Delete
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
@Composable @Composable
fun NewPollOption(pollViewModel: NewPollViewModel, optionIndex: Int) { fun NewPollOption(pollViewModel: NewPostViewModel, optionIndex: Int) {
val colorInValid = TextFieldDefaults.outlinedTextFieldColors( val colorInValid = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = MaterialTheme.colors.error, focusedBorderColor = MaterialTheme.colors.error,
unfocusedBorderColor = Color.Red unfocusedBorderColor = Color.Red
@@ -25,14 +25,26 @@ fun NewPollOption(pollViewModel: NewPollViewModel, optionIndex: Int) {
) )
Row { Row {
val deleteIcon: @Composable (() -> Unit) = {
IconButton(
onClick = {
pollViewModel.pollOptions.remove(optionIndex)
}
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = stringResource(R.string.clear)
)
}
}
OutlinedTextField( OutlinedTextField(
modifier = Modifier modifier = Modifier.weight(1F),
.weight(1F),
value = pollViewModel.pollOptions[optionIndex] ?: "", value = pollViewModel.pollOptions[optionIndex] ?: "",
onValueChange = { pollViewModel.pollOptions[optionIndex] = it }, onValueChange = { pollViewModel.pollOptions[optionIndex] = it },
label = { label = {
Text( 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) 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) 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 @Preview
@Composable @Composable
fun NewPollOptionPreview() { 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.text.style.TextDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R 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 import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
@OptIn(ExperimentalComposeUiApi::class) @OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
fun NewPollPrimaryDescription(pollViewModel: NewPollViewModel) { fun NewPollPrimaryDescription(pollViewModel: NewPostViewModel) {
// initialize focus reference to be able to request focus programmatically // initialize focus reference to be able to request focus programmatically
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
@@ -9,10 +9,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.actions.NewPollViewModel import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
@Composable @Composable
fun NewPollRecipientsField(pollViewModel: NewPollViewModel, account: Account) { fun NewPollRecipientsField(pollViewModel: NewPostViewModel, account: Account) {
// if no recipients, add user's pubkey // if no recipients, add user's pubkey
if (pollViewModel.zapRecipients.isEmpty()) { if (pollViewModel.zapRecipients.isEmpty()) {
pollViewModel.zapRecipients.add(account.userProfile().pubkeyHex) pollViewModel.zapRecipients.add(account.userProfile().pubkeyHex)
@@ -33,7 +33,7 @@ import kotlinx.coroutines.delay
@Composable @Composable
fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = null, account: Account) { fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = null, account: Account) {
val pollViewModel: NewPollViewModel = viewModel() val pollViewModel: NewPostViewModel = viewModel()
val context = LocalContext.current val context = LocalContext.current
@@ -84,7 +84,7 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
PollButton( PollButton(
onPost = { onPost = {
pollViewModel.sendPoll() pollViewModel.sendPost()
onClose() onClose()
}, },
isActive = pollViewModel.message.text.isNotBlank() && 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 import com.vitorpamplona.amethyst.R
@Composable @Composable
fun NewPollVoteValueRange(pollViewModel: NewPollViewModel) { fun NewPollVoteValueRange(pollViewModel: NewPostViewModel) {
var textMax by rememberSaveable { mutableStateOf("") } var textMax by rememberSaveable { mutableStateOf("") }
var textMin by rememberSaveable { mutableStateOf("") } var textMin by rememberSaveable { mutableStateOf("") }
@@ -122,5 +122,5 @@ fun NewPollVoteValueRange(pollViewModel: NewPollViewModel) {
@Preview @Preview
@Composable @Composable
fun NewPollVoteValueRangePreview() { fun NewPollVoteValueRangePreview() {
NewPollVoteValueRange(NewPollViewModel()) NewPollVoteValueRange(NewPostViewModel())
} }
@@ -1,6 +1,8 @@
package com.vitorpamplona.amethyst.ui.actions package com.vitorpamplona.amethyst.ui.actions
import android.widget.Toast import android.widget.Toast
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@@ -104,8 +106,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
postViewModel.sendPost() postViewModel.sendPost()
onClose() onClose()
}, },
isActive = postViewModel.message.text.isNotBlank() && isActive = postViewModel.canPost()
!postViewModel.isUploadingImage
) )
} }
@@ -161,6 +162,26 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content) 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 val myUrlPreview = postViewModel.urlPreview
if (myUrlPreview != null) { if (myUrlPreview != null) {
Row(modifier = Modifier.padding(top = 5.dp)) { Row(modifier = Modifier.padding(top = 5.dp)) {
@@ -217,16 +238,48 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
Row(modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.fillMaxWidth()) {
UploadFromGallery( UploadFromGallery(
isUploading = postViewModel.isUploadingImage, isUploading = postViewModel.isUploadingImage,
tint = MaterialTheme.colors.primary, tint = MaterialTheme.colors.onBackground,
modifier = Modifier.padding(bottom = 10.dp) modifier = Modifier.padding(bottom = 10.dp)
) { ) {
postViewModel.upload(it, context) postViewModel.upload(it, context)
} }
AddPollButton(postViewModel.wantsPoll) {
postViewModel.wantsPoll = !postViewModel.wantsPoll
} }
} }
} }
} }
} }
}
}
@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 @Composable
@@ -3,8 +3,11 @@ package com.vitorpamplona.amethyst.ui.actions
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@@ -34,6 +37,21 @@ open class NewPostViewModel : ViewModel() {
var userSuggestions by mutableStateOf<List<User>>(emptyList()) var userSuggestions by mutableStateOf<List<User>>(emptyList())
var userSuggestionAnchor: TextRange? = null 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?) { open fun load(account: Account, replyingTo: Note?, quote: Note?) {
originalNote = replyingTo originalNote = replyingTo
replyingTo?.let { replyNote -> replyingTo?.let { replyNote ->
@@ -130,16 +148,15 @@ open class NewPostViewModel : ViewModel() {
}.joinToString(" ") }.joinToString(" ")
}.joinToString("\n") }.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) account?.sendChannelMessage(newMessage, originalNote!!.channel()!!.idHex, originalNote!!, mentions)
} else { } else {
account?.sendPost(newMessage, replyTos, mentions) account?.sendPost(newMessage, replyTos, mentions)
} }
message = TextFieldValue("") cancel()
urlPreview = null
isUploadingImage = false
mentions = null
} }
fun upload(it: Uri, context: Context) { fun upload(it: Uri, context: Context) {
@@ -171,6 +188,14 @@ open class NewPostViewModel : ViewModel() {
urlPreview = null urlPreview = null
isUploadingImage = false isUploadingImage = false
mentions = null mentions = null
wantsPoll = false
zapRecipients = mutableStateListOf<HexKey>()
pollOptions = newStateMapPollOptions()
valueMaximum = null
valueMinimum = null
consensusThreshold = null
closedAt = null
} }
open fun findUrlInMessage(): String? { open fun findUrlInMessage(): String? {
@@ -214,4 +239,13 @@ open class NewPostViewModel : ViewModel() {
userSuggestions = emptyList() 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) DisplayUncitedHashtags(noteEvent, eventContent, navController)
} }
/*
TranslateableRichTextViewer(
eventContent,
canPreview = canPreview && !makeItShort,
Modifier.fillMaxWidth(),
noteEvent.tags(),
backgroundColor,
accountViewModel,
navController
)
*/
if (noteEvent is PollNoteEvent) { if (noteEvent is PollNoteEvent) {
PollNote( PollNote(
@@ -16,16 +16,20 @@ import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.saveable.rememberSaveable 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.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext 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.input.KeyboardType 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.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.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.Popup
import androidx.navigation.NavController import androidx.navigation.NavController
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
@@ -35,55 +39,119 @@ import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.* import java.util.*
import kotlin.math.roundToInt
@Composable @Composable
fun PollNote( fun PollNote(
note: Note, baseNote: Note,
canPreview: Boolean, canPreview: Boolean,
backgroundColor: Color, backgroundColor: Color,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
navController: NavController 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() val pollViewModel = PollNoteViewModel()
pollViewModel.load(note) pollViewModel.load(zappedNote)
pollViewModel.pollEvent?.pollOptions()?.forEach { poll_op -> pollViewModel.pollEvent?.pollOptions()?.forEach { poll_op ->
val optionTally = pollViewModel.optionVoteTally(poll_op.key) val optionTally = pollViewModel.optionVoteTally(poll_op.key)
val color = if (
pollViewModel.consensusThreshold != null &&
optionTally >= pollViewModel.consensusThreshold!!
) {
Color.Green.copy(alpha = 0.32f)
} else {
MaterialTheme.colors.primary.copy(alpha = 0.32f)
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 3.dp)
) {
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( Row(
verticalAlignment = Alignment.CenterVertically 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( TranslatableRichTextViewer(
poll_op.value, poll_op.value,
canPreview, canPreview,
modifier = Modifier Modifier.padding(vertical = 5.dp),
.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(), pollViewModel.pollEvent?.tags(),
backgroundColor, backgroundColor,
accountViewModel, accountViewModel,
navController navController
) )
ZapVote(note, accountViewModel, pollViewModel, poll_op.key)
} }
}
Row( }
verticalAlignment = Alignment.CenterVertically },
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)
)
) { ) {
// only show tallies after user has zapped note TranslatableRichTextViewer(
if (note.isZappedBy(accountViewModel.userProfile())) { poll_op.value,
LinearProgressIndicator( canPreview,
modifier = Modifier.width(250.dp), Modifier.padding(vertical = 5.dp, horizontal = 10.dp),
color = if ( pollViewModel.pollEvent?.tags(),
pollViewModel.consensusThreshold != null && backgroundColor,
optionTally >= pollViewModel.consensusThreshold!! accountViewModel,
) { navController
Color.Green )
} else { MaterialTheme.colors.primary }, }
progress = optionTally }
) )
} }
} }
@@ -97,11 +165,10 @@ fun ZapVote(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
pollViewModel: PollNoteViewModel, pollViewModel: PollNoteViewModel,
pollOption: Int, 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 zapsState by baseNote.live().zaps.observeAsState()
val zappedNote = zapsState?.note val zappedNote = zapsState?.note
@@ -112,11 +179,14 @@ fun ZapVote(
var zappingProgress by remember { mutableStateOf(0f) } var zappingProgress by remember { mutableStateOf(0f) }
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
nonClickablePrepend()
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier.combinedClickable(
.then(Modifier.size(20.dp))
.combinedClickable(
role = Role.Button, role = Role.Button,
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false, radius = 24.dp), indication = rememberRipple(bounded = false, radius = 24.dp),
@@ -141,34 +211,63 @@ fun ZapVote(
) )
.show() .show()
} }
} else if (pollViewModel.isVoteAmountAtomic()) { } else if (pollViewModel.isVoteAmountAtomic() && pollViewModel.isPollOptionZappedBy(pollOption, accountViewModel.userProfile())) {
// only allow one vote per option when min==max, i.e. atomic vote amount specified // only allow one vote per option when min==max, i.e. atomic vote amount specified
if (pollViewModel.isPollOptionZappedBy(pollOption, account.userProfile())) {
scope.launch { scope.launch {
Toast Toast
.makeText(context, R.string.one_vote_per_user_on_atomic_votes, Toast.LENGTH_SHORT) .makeText(
context,
R.string.one_vote_per_user_on_atomic_votes,
Toast.LENGTH_SHORT
)
.show() .show()
} }
return@combinedClickable return@combinedClickable
} else { wantsToZap = true } } 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 { } else {
wantsToZap = true wantsToZap = true
} }
}, }
onLongClick = {}
) )
) { ) {
if (wantsToZap) { if (wantsToZap) {
ZapVoteAmountChoicePopup( FilteredZapAmountChoicePopup(
baseNote, baseNote,
accountViewModel, accountViewModel,
pollViewModel, pollViewModel,
pollOption, pollOption,
onDismiss = { onDismiss = {
wantsToZap = false wantsToZap = false
zappingProgress = 0f
},
onChangeAmount = {
wantsToZap = false
}, },
onError = { onError = {
scope.launch { scope.launch {
zappingProgress = 0f
Toast.makeText(context, it, Toast.LENGTH_SHORT).show() 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( Icon(
imageVector = Icons.Default.Bolt, imageVector = Icons.Default.Bolt,
contentDescription = stringResource(R.string.zaps), contentDescription = stringResource(R.string.zaps),
@@ -198,7 +299,7 @@ fun ZapVote(
} }
// only show tallies after a user has zapped note // 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( Text(
showAmount(pollViewModel.zappedPollOptionAmount(pollOption)), showAmount(pollViewModel.zappedPollOptionAmount(pollOption)),
fontSize = 14.sp, 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) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun ZapVoteAmountChoicePopup( fun ZapVoteAmountChoicePopup(
@@ -14,7 +14,7 @@ class PollNoteViewModel {
var pollEvent: PollNoteEvent? = null var pollEvent: PollNoteEvent? = null
private var pollOptions: Map<Int, String>? = null private var pollOptions: Map<Int, String>? = null
var valueMaximum: Int? = null var valueMaximum: Int? = null
private var valueMinimum: Int? = null var valueMinimum: Int? = null
private var closedAt: Int? = null private var closedAt: Int? = null
var consensusThreshold: Float? = null var consensusThreshold: Float? = null
@@ -23,7 +23,7 @@ import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.vitorpamplona.amethyst.buttons.NewChannelButton 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.*
import com.vitorpamplona.amethyst.ui.navigation.AccountSwitchBottomSheet import com.vitorpamplona.amethyst.ui.navigation.AccountSwitchBottomSheet
import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar
@@ -91,7 +91,7 @@ fun FloatingButtons(navController: NavHostController, accountViewModel: AccountS
// Does nothing. // Does nothing.
} }
is AccountState.LoggedIn -> { is AccountState.LoggedIn -> {
FabColumn(state.account) NewNoteButton(state.account)
} }
} }
} }