From 6a85ab743dc4601f12d4e01187ba3943f06e1931 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 3 Apr 2023 20:31:25 -0400 Subject: [PATCH] Moving new pool button to NewPost --- app/build.gradle | 4 - .../amethyst/ui/actions/NewPollClosing.kt | 6 +- .../ui/actions/NewPollConsensusThreshold.kt | 6 +- .../amethyst/ui/actions/NewPollOption.kt | 54 ++- .../ui/actions/NewPollPrimaryDescription.kt | 4 +- .../ui/actions/NewPollRecipientsField.kt | 4 +- .../amethyst/ui/actions/NewPollView.kt | 4 +- .../amethyst/ui/actions/NewPollViewModel.kt | 141 ------- .../ui/actions/NewPollVoteValueRange.kt | 4 +- .../amethyst/ui/actions/NewPostView.kt | 59 ++- .../amethyst/ui/actions/NewPostViewModel.kt | 44 ++- .../amethyst/ui/buttons/NewNoteButton.kt | 47 +++ .../amethyst/ui/note/NoteCompose.kt | 11 - .../amethyst/ui/note/PollNote.kt | 355 ++++++++++++++---- .../amethyst/ui/note/PollNoteViewModel.kt | 2 +- .../amethyst/ui/screen/loggedIn/MainScreen.kt | 4 +- 16 files changed, 459 insertions(+), 290 deletions(-) delete mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollViewModel.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewNoteButton.kt diff --git a/app/build.gradle b/app/build.gradle index d09e47ec4..3f269ebef 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -31,10 +31,6 @@ android { applicationIdSuffix '.debug' versionNameSuffix '-DEBUG' resValue "string", "app_name", "@string/app_name_debug" - - lintOptions{ - disable 'MissingTranslation' - } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollClosing.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollClosing.kt index ec4b7d631..c6d1d1f3e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollClosing.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollClosing.kt @@ -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()) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollConsensusThreshold.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollConsensusThreshold.kt index 21e346f43..d4e4d22bb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollConsensusThreshold.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollConsensusThreshold.kt @@ -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()) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollOption.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollOption.kt index c96af2a23..876134cff 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollOption.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollOption.kt @@ -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) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollPrimaryDescription.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollPrimaryDescription.kt index 1084cf41c..685eb2e8f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollPrimaryDescription.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollPrimaryDescription.kt @@ -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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollRecipientsField.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollRecipientsField.kt index c8aac5cfc..b84ed0480 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollRecipientsField.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollRecipientsField.kt @@ -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) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollView.kt index f16623cce..12977d096 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollView.kt @@ -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() && diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollViewModel.kt deleted file mode 100644 index 2bd10b6a2..000000000 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollViewModel.kt +++ /dev/null @@ -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() - 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() - pollOptions = newStateMapPollOptions() - valueMaximum = null - valueMinimum = null - consensusThreshold = null - closedAt = null - } - - private fun newStateMapPollOptions(): SnapshotStateMap { - return mutableStateMapOf(Pair(0, ""), Pair(1, "")) - } -} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollVoteValueRange.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollVoteValueRange.kt index 63a075759..ee6a3f4fe 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollVoteValueRange.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollVoteValueRange.kt @@ -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()) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index 100b3fabe..22668226b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -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( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index 25635f39b..98453d5d9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -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>(emptyList()) var userSuggestionAnchor: TextRange? = null + // Polls + var wantsPoll by mutableStateOf(false) + var zapRecipients = mutableStateListOf() + 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() + 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 { + return mutableStateMapOf(Pair(0, ""), Pair(1, "")) + } + + fun canPost(): Boolean { + return message.text.isNotBlank() && !isUploadingImage && + (!wantsPoll || pollOptions.values.all { it.isNotEmpty() }) + } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewNoteButton.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewNoteButton.kt new file mode 100644 index 000000000..ece9c89e9 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewNoteButton.kt @@ -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 + ) + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 4db303435..d3639dded 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -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( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt index 91fcd56cd..123ec0b7a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt @@ -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( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt index 2da8418d2..c967261c0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt @@ -14,7 +14,7 @@ class PollNoteViewModel { var pollEvent: PollNoteEvent? = null private var pollOptions: Map? = null var valueMaximum: Int? = null - private var valueMinimum: Int? = null + var valueMinimum: Int? = null private var closedAt: Int? = null var consensusThreshold: Float? = null diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt index df2c14bc5..e0293b5d2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt @@ -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) } } }