Merge pull request #330 from vitorpamplona/pr/321
Moving new pool button to NewPost
This commit is contained in:
@@ -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())
|
||||
}
|
||||
|
||||
+3
-3
@@ -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)
|
||||
}
|
||||
|
||||
+2
-2
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user