From 206d6c68bb06deec62f3bba96218587dfe43a590 Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Sat, 11 Mar 2023 15:13:02 +0900 Subject: [PATCH 01/36] Add new poll button, view, viewModel, icons, string; replace fab with pollButton; supress missing translations errors when debugging --- README.md | 1 + app/build.gradle | 4 + .../amethyst/ui/actions/NewPollView.kt | 270 ++++++++++++++++++ .../amethyst/ui/actions/NewPollViewModel.kt | 174 +++++++++++ .../amethyst/ui/buttons/NewPollButton.kt | 47 +++ .../amethyst/ui/screen/loggedIn/MainScreen.kt | 5 +- app/src/main/res/drawable-hdpi/ic_poll.png | Bin 0 -> 655 bytes app/src/main/res/drawable-mdpi/ic_poll.png | Bin 0 -> 402 bytes app/src/main/res/drawable-xhdpi/ic_poll.png | Bin 0 -> 822 bytes app/src/main/res/drawable-xxhdpi/ic_poll.png | Bin 0 -> 1348 bytes app/src/main/res/drawable-xxxhdpi/ic_poll.png | Bin 0 -> 2072 bytes app/src/main/res/values/strings.xml | 1 + 12 files changed, 500 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollView.kt create 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/NewPollButton.kt create mode 100644 app/src/main/res/drawable-hdpi/ic_poll.png create mode 100644 app/src/main/res/drawable-mdpi/ic_poll.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_poll.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_poll.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_poll.png diff --git a/README.md b/README.md index 522bb9939..a4ddf07ff 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ Amethyst brings the best social network to your Android phone. Just insert your - [ ] Delegated Event Signing (NIP-26) - [ ] Account Creation / Backup Guidance (NIP-06) - [ ] Message Sent feedback (NIP-20) +- [ ] Polls (NIP-69) # Development Overview diff --git a/app/build.gradle b/app/build.gradle index 878ecefcd..7a512c168 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -31,6 +31,10 @@ android { applicationIdSuffix '.debug' versionNameSuffix '-DEBUG' resValue "string", "app_name", "@string/app_name_debug" + + lintOptions{ + disable 'MissingTranslation' + } } } compileOptions { 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 new file mode 100644 index 000000000..f4e67fac6 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollView.kt @@ -0,0 +1,270 @@ +package com.vitorpamplona.amethyst.ui.actions + +import android.widget.Toast +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.style.TextDirection +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.lifecycle.viewmodel.compose.viewModel +import coil.compose.AsyncImage +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.model.TextNoteEvent +import com.vitorpamplona.amethyst.ui.components.* +import com.vitorpamplona.amethyst.ui.note.ReplyInformation +import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine +import kotlinx.coroutines.delay + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = null, account: Account) { + val pollViewModel: NewPollViewModel = viewModel() + + val context = LocalContext.current + + // initialize focus reference to be able to request focus programmatically + val focusRequester = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current + + val scrollState = rememberScrollState() + + LaunchedEffect(Unit) { + pollViewModel.load(account, baseReplyTo, quote) + delay(100) + focusRequester.requestFocus() + + pollViewModel.imageUploadingError.collect { error -> + Toast.makeText(context, error, Toast.LENGTH_SHORT).show() + } + } + + Dialog( + onDismissRequest = { onClose() }, + properties = DialogProperties( + usePlatformDefaultWidth = false, + dismissOnClickOutside = false, + decorFitsSystemWindows = false + ) + ) { + Surface( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + ) { + Column( + modifier = Modifier + .padding(start = 10.dp, end = 10.dp, top = 10.dp) + .imePadding() + .weight(1f) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + ClosePollButton(onCancel = { + pollViewModel.cancel() + onClose() + }) + + PollButton( + onPost = { + pollViewModel.sendPoll() + onClose() + }, + isActive = pollViewModel.message.text.isNotBlank() && + !pollViewModel.isUploadingImage + ) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(scrollState) + ) { + if (pollViewModel.replyTos != null && baseReplyTo?.event is TextNoteEvent) { + ReplyInformation(pollViewModel.replyTos, pollViewModel.mentions, account, "✖ ") { + pollViewModel.removeFromReplyList(it) + } + } + + OutlinedTextField( + value = pollViewModel.message, + onValueChange = { + pollViewModel.updateMessage(it) + }, + keyboardOptions = KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.Sentences + ), + modifier = Modifier + .fillMaxWidth() + .border( + width = 1.dp, + color = MaterialTheme.colors.surface, + shape = RoundedCornerShape(8.dp) + ) + .focusRequester(focusRequester) + .onFocusChanged { + if (it.isFocused) { + keyboardController?.show() + } + }, + placeholder = { + Text( + text = stringResource(R.string.what_s_on_your_mind), + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + }, + colors = TextFieldDefaults + .outlinedTextFieldColors( + unfocusedBorderColor = Color.Transparent, + focusedBorderColor = Color.Transparent + ), + visualTransformation = UrlUserTagTransformation(MaterialTheme.colors.primary), + textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content) + ) + + val myUrlPreview = pollViewModel.urlPreview + if (myUrlPreview != null) { + Row(modifier = Modifier.padding(top = 5.dp)) { + if (isValidURL(myUrlPreview)) { + val removedParamsFromUrl = + myUrlPreview.split("?")[0].lowercase() + if (imageExtension.matcher(removedParamsFromUrl).matches()) { + AsyncImage( + model = myUrlPreview, + contentDescription = myUrlPreview, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .padding(top = 4.dp) + .fillMaxWidth() + .clip(shape = RoundedCornerShape(15.dp)) + .border( + 1.dp, + MaterialTheme.colors.onSurface.copy(alpha = 0.12f), + RoundedCornerShape(15.dp) + ) + ) + } else if (videoExtension.matcher(removedParamsFromUrl) + .matches() + ) { + VideoView(myUrlPreview) + } else { + UrlPreview(myUrlPreview, myUrlPreview) + } + } else if (noProtocolUrlValidator.matcher(myUrlPreview).matches()) { + UrlPreview("https://$myUrlPreview", myUrlPreview) + } + } + } + } + } + + val userSuggestions = pollViewModel.userSuggestions + if (userSuggestions.isNotEmpty()) { + LazyColumn( + contentPadding = PaddingValues( + top = 10.dp + ), + modifier = Modifier.heightIn(0.dp, 300.dp) + ) { + itemsIndexed( + userSuggestions, + key = { _, item -> item.pubkeyHex } + ) { index, item -> + UserLine(item, account) { + pollViewModel.autocompleteWithUser(item) + } + } + } + } + + Row(modifier = Modifier.fillMaxWidth()) { + /*UploadFromGallery( + isUploading = pollViewModel.isUploadingImage + ) { + pollViewModel.upload(it, context) + }*/ + } + } + } + } + } +} + +@Composable +fun ClosePollButton(onCancel: () -> Unit) { + Button( + onClick = { + onCancel() + }, + shape = RoundedCornerShape(20.dp), + colors = ButtonDefaults + .buttonColors( + backgroundColor = Color.Gray + ) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_close), + contentDescription = stringResource(id = R.string.cancel), + modifier = Modifier.size(20.dp), + tint = Color.White + ) + } +} + +@Composable +fun PollButton(modifier: Modifier = Modifier, onPost: () -> Unit = {}, isActive: Boolean) { + Button( + modifier = modifier, + onClick = { + if (isActive) { + onPost() + } + }, + shape = RoundedCornerShape(20.dp), + colors = ButtonDefaults + .buttonColors( + backgroundColor = if (isActive) MaterialTheme.colors.primary else Color.Gray + ) + ) { + Text(text = stringResource(R.string.poll), color = Color.White) + } +} 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 new file mode 100644 index 000000000..f9b72569e --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollViewModel.kt @@ -0,0 +1,174 @@ +package com.vitorpamplona.amethyst.ui.actions + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue +import androidx.lifecycle.ViewModel +import com.vitorpamplona.amethyst.model.* +import com.vitorpamplona.amethyst.service.nip19.Nip19 +import com.vitorpamplona.amethyst.ui.components.isValidURL +import com.vitorpamplona.amethyst.ui.components.noProtocolUrlValidator +import kotlinx.coroutines.flow.MutableSharedFlow + +class NewPollViewModel : ViewModel() { + private var account: Account? = null + private var originalNote: Note? = null + + var mentions by mutableStateOf?>(null) + var replyTos by mutableStateOf?>(null) + + var message by mutableStateOf(TextFieldValue("")) + var urlPreview by mutableStateOf(null) + var isUploadingImage by mutableStateOf(false) + val imageUploadingError = MutableSharedFlow() + + var userSuggestions by mutableStateOf>(emptyList()) + var userSuggestionAnchor: TextRange? = null + + fun load(account: Account, replyingTo: Note?, quote: Note?) { + originalNote = replyingTo + replyingTo?.let { replyNote -> + this.replyTos = (replyNote.replyTo ?: emptyList()).plus(replyNote) + replyNote.author?.let { replyUser -> + val currentMentions = replyNote.mentions ?: emptyList() + if (currentMentions.contains(replyUser)) { + this.mentions = currentMentions + } else { + this.mentions = currentMentions.plus(replyUser) + } + } + } + + quote?.let { + message = TextFieldValue(message.text + "\n\n@${it.idNote()}") + } + + this.account = account + } + + fun addUserToMentions(user: User) { + mentions = if (mentions?.contains(user) == true) mentions else mentions?.plus(user) ?: listOf(user) + } + + fun addNoteToReplyTos(note: Note) { + note.author?.let { addUserToMentions(it) } + replyTos = if (replyTos?.contains(note) == true) replyTos else replyTos?.plus(note) ?: listOf(note) + } + + fun tagIndex(user: User): Int { + // Postr Events assembles replies before mentions in the tag order + return (if (originalNote?.channel() != null) 1 else 0) + (replyTos?.size ?: 0) + (mentions?.indexOf(user) ?: 0) + } + + fun tagIndex(note: Note): Int { + // Postr Events assembles replies before mentions in the tag order + return (if (originalNote?.channel() != null) 1 else 0) + (replyTos?.indexOf(note) ?: 0) + } + + 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?.sendPost(newMessage, replyTos, mentions) + } + + message = TextFieldValue("") + urlPreview = null + isUploadingImage = false + mentions = null + } + + fun cancel() { + message = TextFieldValue("") + urlPreview = null + isUploadingImage = false + mentions = null + } + + fun findUrlInMessage(): String? { + return message.text.split('\n').firstNotNullOfOrNull { paragraph -> + paragraph.split(' ').firstOrNull { word: String -> + isValidURL(word) || noProtocolUrlValidator.matcher(word).matches() + } + } + } + + fun removeFromReplyList(it: User) { + mentions = mentions?.minus(it) + } + + fun updateMessage(it: TextFieldValue) { + message = it + urlPreview = findUrlInMessage() + + if (it.selection.collapsed) { + val lastWord = it.text.substring(0, it.selection.end).substringAfterLast("\n").substringAfterLast(" ") + userSuggestionAnchor = it.selection + if (lastWord.startsWith("@") && lastWord.length > 2) { + userSuggestions = LocalCache.findUsersStartingWith(lastWord.removePrefix("@")) + } else { + userSuggestions = emptyList() + } + } + } + + fun autocompleteWithUser(item: User) { + userSuggestionAnchor?.let { + val lastWord = message.text.substring(0, it.end).substringAfterLast("\n").substringAfterLast(" ") + val lastWordStart = it.end - lastWord.length + val wordToInsert = "@${item.pubkeyNpub()} " + + message = TextFieldValue( + message.text.replaceRange(lastWordStart, it.end, wordToInsert), + TextRange(lastWordStart + wordToInsert.length, lastWordStart + wordToInsert.length) + ) + userSuggestionAnchor = null + userSuggestions = emptyList() + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewPollButton.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewPollButton.kt new file mode 100644 index 000000000..5923caaab --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewPollButton.kt @@ -0,0 +1,47 @@ +package com.vitorpamplona.amethyst.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.NewPollView + +@Composable +fun NewPollButton(account: Account) { + var wantsToPost by remember { + mutableStateOf(false) + } + + if (wantsToPost) { + NewPollView({ 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_poll), + null, + modifier = Modifier.size(26.dp), + tint = Color.White + ) + } +} 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 fa698b275..39f861f59 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 @@ -18,7 +18,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.buttons.NewNoteButton +import com.vitorpamplona.amethyst.buttons.NewPollButton import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar import com.vitorpamplona.amethyst.ui.navigation.AppNavigation import com.vitorpamplona.amethyst.ui.navigation.AppTopBar @@ -71,7 +71,8 @@ fun FloatingButton(navController: NavHostController, accountViewModel: AccountSt // Does nothing. } is AccountState.LoggedIn -> { - NewNoteButton(state.account) + NewPollButton(state.account) + // NewNoteButton(state.account) } } } diff --git a/app/src/main/res/drawable-hdpi/ic_poll.png b/app/src/main/res/drawable-hdpi/ic_poll.png new file mode 100644 index 0000000000000000000000000000000000000000..ff8df419e575bb9dbf77c43618fff3f955989987 GIT binary patch literal 655 zcmV;A0&x9_P)0S zd)l2kE*SfknB7Akc$wtgo#*}S%+74GrBadQoI4jqQI&`%6jjyCa_p+x?cU_z`&O&< zlM{C)5vrw{s{IFyC_N1Ena^)05vt{(TsTtDRy|C!*_;hn|EpBXLy2MkK(sfXJE9KG zpNLd5ox*rOc?1+FH=wk}Qm50o8s-lJG)4ZTHHO3Cac`jS_`jk3;4=G;vj8PYa*W)N zV;`+SzqLxGa$Y{|c6*-OHn|Pj71}$#h6xD6Q$Yt)&~M965A6k(_7ieHXdi3Dd<_$j z)bIB%W8&B|B1_ zt=8-Himzb;(oKl*>$HO@=(|D8$fwuqoyF$?-@mg0mvx~*je`S88h*^m)<|J?m%Vmv z{8xzOa0i#nK;w`$$VQ{F5RQyjEk_kvK1Eq0K=>ysYL^3kY}yH9S5 zDO)W4e(2hOr1U4qjQ$~5LfeJ+(1kHNFS0O_8E705Ak$aR=A~SKM(GIXU-}zRvHnWC pqP=X83iUe1OO#PjURV`HzX0urVIKF}q~!nr002ovPDHLkV1n9nIF|qb literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_poll.png b/app/src/main/res/drawable-mdpi/ic_poll.png new file mode 100644 index 0000000000000000000000000000000000000000..a0282c9f3f920bc658ee1900253cfdc54c014a06 GIT binary patch literal 402 zcmV;D0d4+?P)g-0b2Q9cKn-e9vougYlh4b7 zW5}SZYJo?aReGF4PO>fwt_EqE9_D#I)VbBb_x(NOLlY?IdET}NE`TL@lA8_i`wF#B z8!-7;_}6%-ANmwl15O6=i3t!phUgaiqbQm-T2a%`tI^TA0Zx+DR%|u^;S{xd6VQLh zH3|O_oaZ12M$D}S$j?D|wcrJu@4ZX^6R^wNYS1+BkjGk_(YirV6gy!U?(6(727SI6 zCdkBnHsiJBcXOl3Rs(6+_^|50)MwIvi~Jp{N-F%{4bXUi2`^3H-t~VzPH+MKMDwi% wO#@~})x33sdTbb|t}mRp>npSKOTWwb0_RB!0gk3V*#H0l07*qoM6N<$g6$HvQvd(} literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_poll.png b/app/src/main/res/drawable-xhdpi/ic_poll.png new file mode 100644 index 0000000000000000000000000000000000000000..83191fa48aafe1b44100ed1e3bf225fa154d925d GIT binary patch literal 822 zcmV-61Ihe}P)*AOYz@& zPCKK+HIUM~OJ*ep4x8Sa+I12Tny8H$rD%&> zrBZ2t>-f+wA4zT0NUc(QKzBm?SI_vzgqewd40P?AFl8@FrP2k&&iFuTp(d*jv98n~ znD6}d$nyg-KWb2uoe64{`5@joVYUs>wakwi)Z~1XR%L!{AZ?5dq>W;+cnl|J2fd#{ z!#$bLf`PLz@Cn=iD>AP((C_#6H5!eZ*hn7kmZ(y$t3=LC= zov?Ye0TjK82H(wllHf|a-9BRLu52%&RQAZG)np5gcwW(sbOP#Z`a=sOJq zD>&YeIl_Abn2{1zqH8j4;k|)j@ZJE=0j@fji7R-0S?0Yrz>N&OKXMtx@xII_Vu080 z%h$qy-HuY|xg{u1HTk6~)bks+DtUqQAvi_jkAk+V9FvckG8>WZ$x(Y4N*8l(j07*qoM6N<$f_T)9 AUH||9 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_poll.png b/app/src/main/res/drawable-xxhdpi/ic_poll.png new file mode 100644 index 0000000000000000000000000000000000000000..b792c3025abe58e1277fc34fe0fa9b941090000d GIT binary patch literal 1348 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 XL4jOYQluA&hkC%$3~gF%>=XAp1kC9N literal 0 HcmV?d00001 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f6d729ed9..fee140f6e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -220,5 +220,6 @@ https://twitter.com/<user>/status/<proof post> "<Unable to decrypt private message>\n\nYou were cited in a private/encrypted conversation between %1$s and %2$s." + Poll From 19c3421e92ad66d8759f6a4c3ecebabd83d56a68 Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Sat, 11 Mar 2023 15:20:25 +0900 Subject: [PATCH 02/36] ignore .idea/deploymentTargetDropDown --- .gitignore | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index f928a5380..e1ddbb545 100644 --- a/.gitignore +++ b/.gitignore @@ -8,12 +8,11 @@ /.idea/navEditor.xml /.idea/assetWizardSettings.xml /.idea/androidTestResultsUserPreferences.xml +/.ida/deploymentTargetDropDown.xml .DS_Store /build /captures -.externalNativeBuild .cxx -local.properties # Built application files @@ -55,7 +54,6 @@ proguard/ captures/ # IntelliJ -*.iml .idea/workspace.xml .idea/tasks.xml .idea/gradle.xml From 322853f696a46ccf4b1d7d81b96a48dc54d9efd2 Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Sat, 11 Mar 2023 15:21:13 +0900 Subject: [PATCH 03/36] ignore .idea/deploymentTargetDropDown, remove duplicate ignore rules --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e1ddbb545..829a0c671 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ /.idea/navEditor.xml /.idea/assetWizardSettings.xml /.idea/androidTestResultsUserPreferences.xml -/.ida/deploymentTargetDropDown.xml +/.idea/deploymentTargetDropDown.xml .DS_Store /build /captures From fba873dab9f1070391f339ed73b2d2613fa20b8b Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Sat, 11 Mar 2023 16:57:09 +0900 Subject: [PATCH 04/36] add floating action buttons column --- .../amethyst/ui/buttons/FabColumn.kt | 18 +++++++++++++++ .../amethyst/ui/screen/loggedIn/MainScreen.kt | 22 +++++-------------- 2 files changed, 24 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/FabColumn.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/FabColumn.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/FabColumn.kt new file mode 100644 index 000000000..69601ece1 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/FabColumn.kt @@ -0,0 +1,18 @@ +package com.vitorpamplona.amethyst.buttons + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.model.Account + +@Composable +fun FabColumn(account: Account) { + Column() { + NewPollButton(account) + Spacer(modifier = Modifier.height(20.dp)) + NewNoteButton(account) + } +} 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 39f861f59..46b55c10c 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 @@ -6,25 +6,16 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.material.DrawerValue -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Scaffold -import androidx.compose.material.rememberDrawerState -import androidx.compose.material.rememberScaffoldState +import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController +import com.vitorpamplona.amethyst.buttons.FabColumn import com.vitorpamplona.amethyst.buttons.NewChannelButton -import com.vitorpamplona.amethyst.buttons.NewPollButton -import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar -import com.vitorpamplona.amethyst.ui.navigation.AppNavigation -import com.vitorpamplona.amethyst.ui.navigation.AppTopBar -import com.vitorpamplona.amethyst.ui.navigation.DrawerContent -import com.vitorpamplona.amethyst.ui.navigation.Route -import com.vitorpamplona.amethyst.ui.navigation.currentRoute +import com.vitorpamplona.amethyst.ui.navigation.* import com.vitorpamplona.amethyst.ui.screen.AccountState import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel @@ -47,7 +38,7 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun DrawerContent(navController, scaffoldState, accountViewModel, accountStateViewModel) }, floatingActionButton = { - FloatingButton(navController, accountStateViewModel) + FloatingButtons(navController, accountStateViewModel) }, scaffoldState = scaffoldState ) { @@ -58,7 +49,7 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun } @Composable -fun FloatingButton(navController: NavHostController, accountViewModel: AccountStateViewModel) { +fun FloatingButtons(navController: NavHostController, accountViewModel: AccountStateViewModel) { val accountState by accountViewModel.accountContent.collectAsState() if (currentRoute(navController) == Route.Home.route) { @@ -71,8 +62,7 @@ fun FloatingButton(navController: NavHostController, accountViewModel: AccountSt // Does nothing. } is AccountState.LoggedIn -> { - NewPollButton(state.account) - // NewNoteButton(state.account) + FabColumn(state.account) } } } From 0cf891bb4d3549a5de712cded57f12a41309d15d Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Sat, 11 Mar 2023 18:16:47 +0900 Subject: [PATCH 05/36] pollViewModel -> override postViewModel funs, remove imageUpload from pollView, --- .../amethyst/ui/actions/NewPollView.kt | 43 +---- .../amethyst/ui/actions/NewPollViewModel.kt | 173 +++--------------- .../amethyst/ui/actions/NewPostViewModel.kt | 28 +-- .../amethyst/ui/buttons/NewPollButton.kt | 8 +- app/src/main/res/values/strings.xml | 3 +- 5 files changed, 49 insertions(+), 206 deletions(-) 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 f4e67fac6..05a7c7764 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 @@ -16,12 +16,10 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource @@ -32,7 +30,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.viewmodel.compose.viewModel -import coil.compose.AsyncImage import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note @@ -101,7 +98,7 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n PollButton( onPost = { - pollViewModel.sendPoll() + pollViewModel.sendPost() onClose() }, isActive = pollViewModel.message.text.isNotBlank() && @@ -148,7 +145,7 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n }, placeholder = { Text( - text = stringResource(R.string.what_s_on_your_mind), + text = stringResource(R.string.primary_poll_description), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) }, @@ -160,40 +157,6 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n visualTransformation = UrlUserTagTransformation(MaterialTheme.colors.primary), textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content) ) - - val myUrlPreview = pollViewModel.urlPreview - if (myUrlPreview != null) { - Row(modifier = Modifier.padding(top = 5.dp)) { - if (isValidURL(myUrlPreview)) { - val removedParamsFromUrl = - myUrlPreview.split("?")[0].lowercase() - if (imageExtension.matcher(removedParamsFromUrl).matches()) { - AsyncImage( - model = myUrlPreview, - contentDescription = myUrlPreview, - contentScale = ContentScale.FillWidth, - modifier = Modifier - .padding(top = 4.dp) - .fillMaxWidth() - .clip(shape = RoundedCornerShape(15.dp)) - .border( - 1.dp, - MaterialTheme.colors.onSurface.copy(alpha = 0.12f), - RoundedCornerShape(15.dp) - ) - ) - } else if (videoExtension.matcher(removedParamsFromUrl) - .matches() - ) { - VideoView(myUrlPreview) - } else { - UrlPreview(myUrlPreview, myUrlPreview) - } - } else if (noProtocolUrlValidator.matcher(myUrlPreview).matches()) { - UrlPreview("https://$myUrlPreview", myUrlPreview) - } - } - } } } @@ -265,6 +228,6 @@ fun PollButton(modifier: Modifier = Modifier, onPost: () -> Unit = {}, isActive: backgroundColor = if (isActive) MaterialTheme.colors.primary else Color.Gray ) ) { - Text(text = stringResource(R.string.poll), color = Color.White) + Text(text = stringResource(R.string.post_poll), color = Color.White) } } 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 index f9b72569e..39b43f96b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollViewModel.kt @@ -1,174 +1,53 @@ package com.vitorpamplona.amethyst.ui.actions -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue -import androidx.lifecycle.ViewModel -import com.vitorpamplona.amethyst.model.* -import com.vitorpamplona.amethyst.service.nip19.Nip19 -import com.vitorpamplona.amethyst.ui.components.isValidURL -import com.vitorpamplona.amethyst.ui.components.noProtocolUrlValidator -import kotlinx.coroutines.flow.MutableSharedFlow +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User -class NewPollViewModel : ViewModel() { - private var account: Account? = null - private var originalNote: Note? = null +class NewPollViewModel : NewPostViewModel() { - var mentions by mutableStateOf?>(null) - var replyTos by mutableStateOf?>(null) - - var message by mutableStateOf(TextFieldValue("")) - var urlPreview by mutableStateOf(null) - var isUploadingImage by mutableStateOf(false) - val imageUploadingError = MutableSharedFlow() - - var userSuggestions by mutableStateOf>(emptyList()) - var userSuggestionAnchor: TextRange? = null - - fun load(account: Account, replyingTo: Note?, quote: Note?) { - originalNote = replyingTo - replyingTo?.let { replyNote -> - this.replyTos = (replyNote.replyTo ?: emptyList()).plus(replyNote) - replyNote.author?.let { replyUser -> - val currentMentions = replyNote.mentions ?: emptyList() - if (currentMentions.contains(replyUser)) { - this.mentions = currentMentions - } else { - this.mentions = currentMentions.plus(replyUser) - } - } - } - - quote?.let { - message = TextFieldValue(message.text + "\n\n@${it.idNote()}") - } - - this.account = account + override fun load(account: Account, replyingTo: Note?, quote: Note?) { + super.load(account, replyingTo, quote) } - fun addUserToMentions(user: User) { - mentions = if (mentions?.contains(user) == true) mentions else mentions?.plus(user) ?: listOf(user) + override fun addUserToMentions(user: User) { + super.addUserToMentions(user) } - fun addNoteToReplyTos(note: Note) { - note.author?.let { addUserToMentions(it) } - replyTos = if (replyTos?.contains(note) == true) replyTos else replyTos?.plus(note) ?: listOf(note) + override fun addNoteToReplyTos(note: Note) { + super.addNoteToReplyTos(note) } - fun tagIndex(user: User): Int { - // Postr Events assembles replies before mentions in the tag order - return (if (originalNote?.channel() != null) 1 else 0) + (replyTos?.size ?: 0) + (mentions?.indexOf(user) ?: 0) + override fun tagIndex(user: User): Int { + return super.tagIndex(user) } - fun tagIndex(note: Note): Int { - // Postr Events assembles replies before mentions in the tag order - return (if (originalNote?.channel() != null) 1 else 0) + (replyTos?.indexOf(note) ?: 0) + 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?.sendPost(newMessage, replyTos, mentions) - } - - message = TextFieldValue("") - urlPreview = null - isUploadingImage = false - mentions = null + override fun sendPost() { + super.sendPost() } - fun cancel() { - message = TextFieldValue("") - urlPreview = null - isUploadingImage = false - mentions = null + override fun cancel() { + super.cancel() } - fun findUrlInMessage(): String? { - return message.text.split('\n').firstNotNullOfOrNull { paragraph -> - paragraph.split(' ').firstOrNull { word: String -> - isValidURL(word) || noProtocolUrlValidator.matcher(word).matches() - } - } + override fun findUrlInMessage(): String? { + return super.findUrlInMessage() } - fun removeFromReplyList(it: User) { - mentions = mentions?.minus(it) + override fun removeFromReplyList(it: User) { + super.removeFromReplyList(it) } - fun updateMessage(it: TextFieldValue) { - message = it - urlPreview = findUrlInMessage() - - if (it.selection.collapsed) { - val lastWord = it.text.substring(0, it.selection.end).substringAfterLast("\n").substringAfterLast(" ") - userSuggestionAnchor = it.selection - if (lastWord.startsWith("@") && lastWord.length > 2) { - userSuggestions = LocalCache.findUsersStartingWith(lastWord.removePrefix("@")) - } else { - userSuggestions = emptyList() - } - } + override fun updateMessage(it: TextFieldValue) { + super.updateMessage(it) } - fun autocompleteWithUser(item: User) { - userSuggestionAnchor?.let { - val lastWord = message.text.substring(0, it.end).substringAfterLast("\n").substringAfterLast(" ") - val lastWordStart = it.end - lastWord.length - val wordToInsert = "@${item.pubkeyNpub()} " - - message = TextFieldValue( - message.text.replaceRange(lastWordStart, it.end, wordToInsert), - TextRange(lastWordStart + wordToInsert.length, lastWordStart + wordToInsert.length) - ) - userSuggestionAnchor = null - userSuggestions = emptyList() - } + override fun autocompleteWithUser(item: User) { + super.autocompleteWithUser(item) } } 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 1df19f6c3..98feff723 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 @@ -16,9 +16,9 @@ import com.vitorpamplona.amethyst.ui.components.noProtocolUrlValidator import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch -class NewPostViewModel : ViewModel() { - private var account: Account? = null - private var originalNote: Note? = null +open class NewPostViewModel : ViewModel() { + var account: Account? = null + var originalNote: Note? = null var mentions by mutableStateOf?>(null) var replyTos by mutableStateOf?>(null) @@ -31,7 +31,7 @@ class NewPostViewModel : ViewModel() { var userSuggestions by mutableStateOf>(emptyList()) var userSuggestionAnchor: TextRange? = null - fun load(account: Account, replyingTo: Note?, quote: Note?) { + open fun load(account: Account, replyingTo: Note?, quote: Note?) { originalNote = replyingTo replyingTo?.let { replyNote -> this.replyTos = (replyNote.replyTo ?: emptyList()).plus(replyNote) @@ -52,26 +52,26 @@ class NewPostViewModel : ViewModel() { this.account = account } - fun addUserToMentions(user: User) { + open fun addUserToMentions(user: User) { mentions = if (mentions?.contains(user) == true) mentions else mentions?.plus(user) ?: listOf(user) } - fun addNoteToReplyTos(note: Note) { + open fun addNoteToReplyTos(note: Note) { note.author?.let { addUserToMentions(it) } replyTos = if (replyTos?.contains(note) == true) replyTos else replyTos?.plus(note) ?: listOf(note) } - fun tagIndex(user: User): Int { + open fun tagIndex(user: User): Int { // Postr Events assembles replies before mentions in the tag order return (if (originalNote?.channel() != null) 1 else 0) + (replyTos?.size ?: 0) + (mentions?.indexOf(user) ?: 0) } - fun tagIndex(note: Note): Int { + open fun tagIndex(note: Note): Int { // Postr Events assembles replies before mentions in the tag order return (if (originalNote?.channel() != null) 1 else 0) + (replyTos?.indexOf(note) ?: 0) } - fun sendPost() { + open fun sendPost() { // adds all references to mentions and reply tos message.text.split('\n').forEach { paragraph: String -> paragraph.split(' ').forEach { word: String -> @@ -147,14 +147,14 @@ class NewPostViewModel : ViewModel() { ) } - fun cancel() { + open fun cancel() { message = TextFieldValue("") urlPreview = null isUploadingImage = false mentions = null } - fun findUrlInMessage(): String? { + open fun findUrlInMessage(): String? { return message.text.split('\n').firstNotNullOfOrNull { paragraph -> paragraph.split(' ').firstOrNull { word: String -> isValidURL(word) || noProtocolUrlValidator.matcher(word).matches() @@ -162,11 +162,11 @@ class NewPostViewModel : ViewModel() { } } - fun removeFromReplyList(it: User) { + open fun removeFromReplyList(it: User) { mentions = mentions?.minus(it) } - fun updateMessage(it: TextFieldValue) { + open fun updateMessage(it: TextFieldValue) { message = it urlPreview = findUrlInMessage() @@ -181,7 +181,7 @@ class NewPostViewModel : ViewModel() { } } - fun autocompleteWithUser(item: User) { + open fun autocompleteWithUser(item: User) { userSuggestionAnchor?.let { val lastWord = message.text.substring(0, it.end).substringAfterLast("\n").substringAfterLast(" ") val lastWordStart = it.end - lastWord.length diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewPollButton.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewPollButton.kt index 5923caaab..5169f5b1e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewPollButton.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewPollButton.kt @@ -22,16 +22,16 @@ import com.vitorpamplona.amethyst.ui.actions.NewPollView @Composable fun NewPollButton(account: Account) { - var wantsToPost by remember { + var wantsToPoll by remember { mutableStateOf(false) } - if (wantsToPost) { - NewPollView({ wantsToPost = false }, account = account) + if (wantsToPoll) { + NewPollView({ wantsToPoll = false }, account = account) } OutlinedButton( - onClick = { wantsToPost = true }, + onClick = { wantsToPoll = true }, modifier = Modifier.size(55.dp), shape = CircleShape, colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.primary), diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fee140f6e..5a4a6a4e8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -220,6 +220,7 @@ https://twitter.com/<user>/status/<proof post> "<Unable to decrypt private message>\n\nYou were cited in a private/encrypted conversation between %1$s and %2$s." - Poll + Post Poll + Primary poll description… From 564f926213431ba59a8c129ee739c43094711855 Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Sat, 11 Mar 2023 18:57:02 +0900 Subject: [PATCH 06/36] remove closePollButton --- .../amethyst/ui/actions/NewPollView.kt | 24 +------------------ 1 file changed, 1 insertion(+), 23 deletions(-) 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 05a7c7764..9b3f18bec 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 @@ -22,7 +22,6 @@ import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextDirection @@ -91,7 +90,7 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - ClosePollButton(onCancel = { + CloseButton(onCancel = { pollViewModel.cancel() onClose() }) @@ -192,27 +191,6 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n } } -@Composable -fun ClosePollButton(onCancel: () -> Unit) { - Button( - onClick = { - onCancel() - }, - shape = RoundedCornerShape(20.dp), - colors = ButtonDefaults - .buttonColors( - backgroundColor = Color.Gray - ) - ) { - Icon( - painter = painterResource(id = R.drawable.ic_close), - contentDescription = stringResource(id = R.string.cancel), - modifier = Modifier.size(20.dp), - tint = Color.White - ) - } -} - @Composable fun PollButton(modifier: Modifier = Modifier, onPost: () -> Unit = {}, isActive: Boolean) { Button( From 00f9f7ba5281a0bd450ac780f1fa447e74d3f1db Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Mon, 13 Mar 2023 13:15:40 +0900 Subject: [PATCH 07/36] add PollPrimaryDescription, PollOption objects --- .../amethyst/ui/actions/NewPollView.kt | 58 ++-------------- .../amethyst/ui/components/PollOption.kt | 36 ++++++++++ .../ui/components/PollPrimaryDescription.kt | 67 +++++++++++++++++++ app/src/main/res/values/strings.xml | 2 + 4 files changed, 111 insertions(+), 52 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollPrimaryDescription.kt 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 9b3f18bec..1db3668ea 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 @@ -1,30 +1,20 @@ package com.vitorpamplona.amethyst.ui.actions import android.widget.Toast -import androidx.compose.foundation.border import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties @@ -33,28 +23,23 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.TextNoteEvent -import com.vitorpamplona.amethyst.ui.components.* +import com.vitorpamplona.amethyst.ui.components.PollOption +import com.vitorpamplona.amethyst.ui.components.PollPrimaryDescription import com.vitorpamplona.amethyst.ui.note.ReplyInformation import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine import kotlinx.coroutines.delay -@OptIn(ExperimentalComposeUiApi::class) @Composable fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = null, account: Account) { val pollViewModel: NewPollViewModel = viewModel() val context = LocalContext.current - // initialize focus reference to be able to request focus programmatically - val focusRequester = remember { FocusRequester() } - val keyboardController = LocalSoftwareKeyboardController.current - val scrollState = rememberScrollState() LaunchedEffect(Unit) { pollViewModel.load(account, baseReplyTo, quote) delay(100) - focusRequester.requestFocus() pollViewModel.imageUploadingError.collect { error -> Toast.makeText(context, error, Toast.LENGTH_SHORT).show() @@ -121,41 +106,10 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n } } - OutlinedTextField( - value = pollViewModel.message, - onValueChange = { - pollViewModel.updateMessage(it) - }, - keyboardOptions = KeyboardOptions.Default.copy( - capitalization = KeyboardCapitalization.Sentences - ), - modifier = Modifier - .fillMaxWidth() - .border( - width = 1.dp, - color = MaterialTheme.colors.surface, - shape = RoundedCornerShape(8.dp) - ) - .focusRequester(focusRequester) - .onFocusChanged { - if (it.isFocused) { - keyboardController?.show() - } - }, - placeholder = { - Text( - text = stringResource(R.string.primary_poll_description), - color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) - ) - }, - colors = TextFieldDefaults - .outlinedTextFieldColors( - unfocusedBorderColor = Color.Transparent, - focusedBorderColor = Color.Transparent - ), - visualTransformation = UrlUserTagTransformation(MaterialTheme.colors.primary), - textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content) - ) + PollPrimaryDescription(pollViewModel = pollViewModel) + + PollOption(pollViewModel, 0) + PollOption(pollViewModel, 1) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt new file mode 100644 index 000000000..ca335ecbb --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt @@ -0,0 +1,36 @@ +package com.vitorpamplona.amethyst.ui.components + +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.res.stringResource +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.actions.NewPollViewModel + +@Composable +fun PollOption(pollViewModel: NewPollViewModel, optionIndex: Int) { + var text by rememberSaveable() { mutableStateOf("") } + + OutlinedTextField( + value = text, + onValueChange = { text = it }, + label = { + Text( + text = stringResource(R.string.poll_option_index).format(optionIndex), + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + }, + placeholder = { + Text( + text = stringResource(R.string.what_s_on_your_mind), + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + } + + ) +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollPrimaryDescription.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollPrimaryDescription.kt new file mode 100644 index 000000000..8870b42ec --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollPrimaryDescription.kt @@ -0,0 +1,67 @@ +package com.vitorpamplona.amethyst.ui.components + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +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.UrlUserTagTransformation + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun PollPrimaryDescription(pollViewModel: NewPollViewModel) { + // initialize focus reference to be able to request focus programmatically + val focusRequester = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current + + OutlinedTextField( + value = pollViewModel.message, + onValueChange = { + pollViewModel.updateMessage(it) + }, + keyboardOptions = KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.Sentences + ), + modifier = Modifier + .fillMaxWidth() + .border( + width = 1.dp, + color = MaterialTheme.colors.surface, + shape = RoundedCornerShape(8.dp) + ) + .focusRequester(focusRequester) + .onFocusChanged { + if (it.isFocused) { + keyboardController?.show() + } + }, + placeholder = { + Text( + text = stringResource(R.string.primary_poll_description), + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + }, + colors = TextFieldDefaults + .outlinedTextFieldColors( + unfocusedBorderColor = Color.Transparent, + focusedBorderColor = Color.Transparent + ), + visualTransformation = UrlUserTagTransformation(MaterialTheme.colors.primary), + textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content) + ) +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5a4a6a4e8..db4332bdc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -222,5 +222,7 @@ "<Unable to decrypt private message>\n\nYou were cited in a private/encrypted conversation between %1$s and %2$s." Post Poll Primary poll description… + Poll option description + Option %s From e30b4f7c7e82d4ea950b7750917498126eea0f91 Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Mon, 13 Mar 2023 14:39:40 +0900 Subject: [PATCH 08/36] add PollRecipientsField, add buttons for add/delete options, format poll text fields, change poll option placeholder string rename primaryPollDescription string --- .../amethyst/ui/actions/NewPollView.kt | 21 ++++++- .../amethyst/ui/components/PollOption.kt | 59 +++++++++++++------ .../ui/components/PollPrimaryDescription.kt | 12 +++- .../ui/components/PollRecipientsField.kt | 35 +++++++++++ app/src/main/res/values/strings.xml | 3 +- 5 files changed, 109 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollRecipientsField.kt 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 1db3668ea..7f142094a 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 @@ -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.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed @@ -14,6 +16,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog @@ -25,6 +28,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.ui.components.PollOption import com.vitorpamplona.amethyst.ui.components.PollPrimaryDescription +import com.vitorpamplona.amethyst.ui.components.PollRecipientsField import com.vitorpamplona.amethyst.ui.note.ReplyInformation import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine import kotlinx.coroutines.delay @@ -37,6 +41,8 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n val scrollState = rememberScrollState() + var pollOptionList = listOf() + LaunchedEffect(Unit) { pollViewModel.load(account, baseReplyTo, quote) delay(100) @@ -106,10 +112,23 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n } } + PollRecipientsField() PollPrimaryDescription(pollViewModel = pollViewModel) - PollOption(pollViewModel, 0) PollOption(pollViewModel, 1) + Button( + onClick = { /*TODO*/ }, + 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) + ) + } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt index ca335ecbb..888c23cdf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt @@ -1,14 +1,21 @@ package com.vitorpamplona.amethyst.ui.components -import androidx.compose.material.MaterialTheme -import androidx.compose.material.OutlinedTextField -import androidx.compose.material.Text +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.actions.NewPollViewModel @@ -16,21 +23,39 @@ import com.vitorpamplona.amethyst.ui.actions.NewPollViewModel fun PollOption(pollViewModel: NewPollViewModel, optionIndex: Int) { var text by rememberSaveable() { mutableStateOf("") } - OutlinedTextField( - value = text, - onValueChange = { text = it }, - label = { - Text( - text = stringResource(R.string.poll_option_index).format(optionIndex), - color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + Row() { + OutlinedTextField( + value = text, + onValueChange = { text = it }, + label = { + Text( + text = stringResource(R.string.poll_option_index).format(optionIndex), + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + }, + placeholder = { + Text( + text = stringResource(R.string.poll_option_description), + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + } + + ) + Button( + modifier = Modifier + .padding(start = 6.dp, top = 2.dp) + .imePadding(), + onClick = { /*TODO*/ }, + border = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f)), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) - }, - placeholder = { - Text( - text = stringResource(R.string.what_s_on_your_mind), - color = 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) ) } - - ) + } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollPrimaryDescription.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollPrimaryDescription.kt index 8870b42ec..6f65c8266 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollPrimaryDescription.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollPrimaryDescription.kt @@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.components import androidx.compose.foundation.border import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.* @@ -34,14 +35,21 @@ fun PollPrimaryDescription(pollViewModel: NewPollViewModel) { onValueChange = { pollViewModel.updateMessage(it) }, + label = { + Text( + text = stringResource(R.string.poll_primary_description), + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + }, keyboardOptions = KeyboardOptions.Default.copy( capitalization = KeyboardCapitalization.Sentences ), modifier = Modifier .fillMaxWidth() + .padding(top = 8.dp) .border( width = 1.dp, - color = MaterialTheme.colors.surface, + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), shape = RoundedCornerShape(8.dp) ) .focusRequester(focusRequester) @@ -52,7 +60,7 @@ fun PollPrimaryDescription(pollViewModel: NewPollViewModel) { }, placeholder = { Text( - text = stringResource(R.string.primary_poll_description), + text = stringResource(R.string.poll_primary_description), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) }, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollRecipientsField.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollRecipientsField.kt new file mode 100644 index 000000000..0eb3b00c0 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollRecipientsField.kt @@ -0,0 +1,35 @@ +package com.vitorpamplona.amethyst.ui.components + +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.res.stringResource +import com.vitorpamplona.amethyst.R + +@Composable +fun PollRecipientsField() { + var text by rememberSaveable() { mutableStateOf("") } + + OutlinedTextField( + value = text, + onValueChange = { text = it }, + label = { + Text( + text = stringResource(R.string.poll_recipients), + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + }, + placeholder = { + Text( + text = stringResource(R.string.poll_recipients), + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + } + + ) +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index db4332bdc..17ed0b534 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -221,8 +221,9 @@ "<Unable to decrypt private message>\n\nYou were cited in a private/encrypted conversation between %1$s and %2$s." Post Poll - Primary poll description… + Primary poll description… Poll option description Option %s + Poll recipients From 851ec71f4a97d616054a08a5464dfdbccd78302f Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Mon, 13 Mar 2023 15:46:48 +0900 Subject: [PATCH 09/36] add PollVoteValueRange component, add PollOption, PollVoteValueRange component previews, rename poll_recipients string --- .../amethyst/ui/actions/NewPollView.kt | 13 +++- .../amethyst/ui/components/PollOption.kt | 10 ++- .../ui/components/PollRecipientsField.kt | 4 +- .../ui/components/PollVoteValueRange.kt | 75 +++++++++++++++++++ app/src/main/res/values/strings.xml | 5 +- 5 files changed, 100 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollVoteValueRange.kt 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 7f142094a..06727d56f 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 @@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.ui.components.PollOption import com.vitorpamplona.amethyst.ui.components.PollPrimaryDescription import com.vitorpamplona.amethyst.ui.components.PollRecipientsField +import com.vitorpamplona.amethyst.ui.components.PollVoteValueRange import com.vitorpamplona.amethyst.ui.note.ReplyInformation import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine import kotlinx.coroutines.delay @@ -114,8 +115,8 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n PollRecipientsField() PollPrimaryDescription(pollViewModel = pollViewModel) - PollOption(pollViewModel, 0) - PollOption(pollViewModel, 1) + PollOption(0) + PollOption(1) Button( onClick = { /*TODO*/ }, border = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f)), @@ -129,6 +130,8 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n modifier = Modifier.size(18.dp) ) } + Text(stringResource(R.string.poll_heading_optional)) + PollVoteValueRange() } } @@ -182,3 +185,9 @@ fun PollButton(modifier: Modifier = Modifier, onPost: () -> Unit = {}, isActive: Text(text = stringResource(R.string.post_poll), color = Color.White) } } + +/*@Preview +@Composable +fun NewPollViewPreview() { + NewPollView(onClose = {}, account = Account(loggedIn = Persona())) +}*/ diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt index 888c23cdf..2a935379d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt @@ -15,12 +15,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +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 @Composable -fun PollOption(pollViewModel: NewPollViewModel, optionIndex: Int) { +fun PollOption(optionIndex: Int) { var text by rememberSaveable() { mutableStateOf("") } Row() { @@ -59,3 +59,9 @@ fun PollOption(pollViewModel: NewPollViewModel, optionIndex: Int) { } } } + +@Preview +@Composable +fun PollOptionPreview() { + PollOption(0) +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollRecipientsField.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollRecipientsField.kt index 0eb3b00c0..ed135f09c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollRecipientsField.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollRecipientsField.kt @@ -20,13 +20,13 @@ fun PollRecipientsField() { onValueChange = { text = it }, label = { Text( - text = stringResource(R.string.poll_recipients), + text = stringResource(R.string.poll_zap_recipients), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) }, placeholder = { Text( - text = stringResource(R.string.poll_recipients), + text = stringResource(R.string.poll_zap_recipients), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollVoteValueRange.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollVoteValueRange.kt new file mode 100644 index 000000000..b4064d379 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollVoteValueRange.kt @@ -0,0 +1,75 @@ +package com.vitorpamplona.amethyst.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +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 + +@Composable +fun PollVoteValueRange() { + var minText by rememberSaveable { mutableStateOf("") } + var maxText by rememberSaveable { mutableStateOf("") } + + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + OutlinedTextField( + value = minText, + onValueChange = { minText = it }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.width(150.dp), + label = { + Text( + text = stringResource(R.string.poll_vote_value_min), + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + }, + placeholder = { + Text( + text = stringResource(R.string.poll_vote_value_min), + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + } + ) + OutlinedTextField( + value = maxText, + onValueChange = { maxText = it }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.width(150.dp), + label = { + Text( + text = stringResource(R.string.poll_vote_value_max), + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + }, + placeholder = { + Text( + text = stringResource(R.string.poll_vote_value_max), + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + } + ) + } +} + +@Preview +@Composable +fun PollVoteValueRangePreview() { + PollVoteValueRange() +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 17ed0b534..6639bf010 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -224,6 +224,9 @@ Primary poll description… Poll option description Option %s - Poll recipients + Zap recipients + Optional: + Vote minimum + Vote maximum From c31b99b1dc4e971bb477b913090a9128c97a3571 Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Mon, 13 Mar 2023 16:32:38 +0900 Subject: [PATCH 10/36] add consensus threshold component/preview, change value range placeholder strings --- .../amethyst/ui/actions/NewPollView.kt | 7 ++- .../ui/components/PollConsensusThreshold.kt | 54 +++++++++++++++++++ .../ui/components/PollVoteValueRange.kt | 4 +- app/src/main/res/values/strings.xml | 13 +++-- 4 files changed, 68 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollConsensusThreshold.kt 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 06727d56f..881666593 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 @@ -26,10 +26,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.TextNoteEvent -import com.vitorpamplona.amethyst.ui.components.PollOption -import com.vitorpamplona.amethyst.ui.components.PollPrimaryDescription -import com.vitorpamplona.amethyst.ui.components.PollRecipientsField -import com.vitorpamplona.amethyst.ui.components.PollVoteValueRange +import com.vitorpamplona.amethyst.ui.components.* import com.vitorpamplona.amethyst.ui.note.ReplyInformation import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine import kotlinx.coroutines.delay @@ -113,6 +110,7 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n } } + Text(stringResource(R.string.poll_heading_required)) PollRecipientsField() PollPrimaryDescription(pollViewModel = pollViewModel) PollOption(0) @@ -132,6 +130,7 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n } Text(stringResource(R.string.poll_heading_optional)) PollVoteValueRange() + PollConsensusThreshold() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollConsensusThreshold.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollConsensusThreshold.kt new file mode 100644 index 000000000..6debc95ff --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollConsensusThreshold.kt @@ -0,0 +1,54 @@ +package com.vitorpamplona.amethyst.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +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 + +@Composable +fun PollConsensusThreshold() { + var text by rememberSaveable { mutableStateOf("") } + + Row( + horizontalArrangement = Arrangement.Center + ) { + OutlinedTextField( + value = text, + onValueChange = { text = it }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.width(150.dp), + label = { + Text( + text = stringResource(R.string.poll_consensus_threshold), + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + }, + placeholder = { + Text( + text = stringResource(R.string.poll_consensus_threshold_percent), + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + } + ) + } +} + +@Preview +@Composable +fun PollConsensusThresholdPreview() { + PollConsensusThreshold() +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollVoteValueRange.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollVoteValueRange.kt index b4064d379..c4240b237 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollVoteValueRange.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollVoteValueRange.kt @@ -42,7 +42,7 @@ fun PollVoteValueRange() { }, placeholder = { Text( - text = stringResource(R.string.poll_vote_value_min), + text = stringResource(R.string.poll_vote_value_min_zap_amount), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) } @@ -60,7 +60,7 @@ fun PollVoteValueRange() { }, placeholder = { Text( - text = stringResource(R.string.poll_vote_value_max), + text = stringResource(R.string.poll_vote_value_max_zap_amount), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6639bf010..c4bda6c0a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -221,12 +221,17 @@ "<Unable to decrypt private message>\n\nYou were cited in a private/encrypted conversation between %1$s and %2$s." Post Poll - Primary poll description… - Poll option description - Option %s + Required fields: Zap recipients - Optional: + Primary poll description… + Option %s + Poll option description + Optional fields: Vote minimum Vote maximum + Consensus threshold + % + Minimum zap + Maximum zap From 63ad7fd205d62d144c762a5a533d7d1acee5e6e8 Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Mon, 13 Mar 2023 17:33:00 +0900 Subject: [PATCH 11/36] add PollClosing component, rename poll strings/values --- .../amethyst/ui/actions/NewPollView.kt | 1 + .../amethyst/ui/components/PollClosing.kt | 56 +++++++++++++++++++ .../ui/components/PollConsensusThreshold.kt | 2 + .../ui/components/PollVoteValueRange.kt | 8 +-- app/src/main/res/values/strings.xml | 11 ++-- 5 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollClosing.kt 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 881666593..79910b275 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 @@ -131,6 +131,7 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n Text(stringResource(R.string.poll_heading_optional)) PollVoteValueRange() PollConsensusThreshold() + PollClosing() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollClosing.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollClosing.kt new file mode 100644 index 000000000..2694e0ff2 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollClosing.kt @@ -0,0 +1,56 @@ +package com.vitorpamplona.amethyst.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +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 + +@Composable +fun PollClosing() { + var text by rememberSaveable { mutableStateOf("") } + + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + OutlinedTextField( + value = text, + onValueChange = { text = it }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.width(150.dp), + label = { + Text( + text = stringResource(R.string.poll_closing_time), + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + }, + placeholder = { + Text( + text = stringResource(R.string.poll_closing_time_days), + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + } + ) + } +} + +@Preview +@Composable +fun PollClosingPreview() { + PollClosing() +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollConsensusThreshold.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollConsensusThreshold.kt index 6debc95ff..67296bd94 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollConsensusThreshold.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollConsensusThreshold.kt @@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.width import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.MaterialTheme @@ -24,6 +25,7 @@ fun PollConsensusThreshold() { var text by rememberSaveable { mutableStateOf("") } Row( + Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center ) { OutlinedTextField( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollVoteValueRange.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollVoteValueRange.kt index c4240b237..63d8881eb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollVoteValueRange.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollVoteValueRange.kt @@ -36,13 +36,13 @@ fun PollVoteValueRange() { modifier = Modifier.width(150.dp), label = { Text( - text = stringResource(R.string.poll_vote_value_min), + text = stringResource(R.string.poll_zap_value_min), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) }, placeholder = { Text( - text = stringResource(R.string.poll_vote_value_min_zap_amount), + text = stringResource(R.string.poll_zap_amount), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) } @@ -54,13 +54,13 @@ fun PollVoteValueRange() { modifier = Modifier.width(150.dp), label = { Text( - text = stringResource(R.string.poll_vote_value_max), + text = stringResource(R.string.poll_zap_value_max), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) }, placeholder = { Text( - text = stringResource(R.string.poll_vote_value_max_zap_amount), + text = stringResource(R.string.poll_zap_amount), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c4bda6c0a..bba5e2191 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -227,11 +227,12 @@ Option %s Poll option description Optional fields: - Vote minimum - Vote maximum - Consensus threshold + Zap minimum + Zap maximum + Consensus % - Minimum zap - Maximum zap + sats + Close after + days From 107e09aefbd9c52a640a4afbda8a5516f891ece3 Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Thu, 16 Mar 2023 19:06:00 +0900 Subject: [PATCH 12/36] move pollOptions list to pollViewModel, add/delete poll options functionality, don't allow deleting first 2 poll options --- .../amethyst/ui/actions/NewPollView.kt | 9 ++-- .../amethyst/ui/actions/NewPollViewModel.kt | 3 ++ .../amethyst/ui/components/PollOption.kt | 47 +++++++++---------- 3 files changed, 29 insertions(+), 30 deletions(-) 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 79910b275..3cd3caf28 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 @@ -39,8 +39,6 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n val scrollState = rememberScrollState() - var pollOptionList = listOf() - LaunchedEffect(Unit) { pollViewModel.load(account, baseReplyTo, quote) delay(100) @@ -113,10 +111,11 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n Text(stringResource(R.string.poll_heading_required)) PollRecipientsField() PollPrimaryDescription(pollViewModel = pollViewModel) - PollOption(0) - PollOption(1) + pollViewModel.pollOptions.forEachIndexed { index, element -> + PollOption(pollViewModel, index) + } Button( - onClick = { /*TODO*/ }, + onClick = { pollViewModel.pollOptions.add("") }, border = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f)), colors = ButtonDefaults.outlinedButtonColors( contentColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) 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 index 39b43f96b..e13bc4ca8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollViewModel.kt @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst.ui.actions +import androidx.compose.runtime.mutableStateListOf import androidx.compose.ui.text.input.TextFieldValue import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note @@ -7,6 +8,8 @@ import com.vitorpamplona.amethyst.model.User class NewPollViewModel : NewPostViewModel() { + var pollOptions = mutableStateListOf("", "") + override fun load(account: Account, replyingTo: Note?, quote: Note?) { super.load(account, replyingTo, quote) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt index 2a935379d..ab2b1d134 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt @@ -8,25 +8,20 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.* import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource 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 @Composable -fun PollOption(optionIndex: Int) { - var text by rememberSaveable() { mutableStateOf("") } - +fun PollOption(pollViewModel: NewPollViewModel, optionIndex: Int) { Row() { OutlinedTextField( - value = text, - onValueChange = { text = it }, + value = pollViewModel.pollOptions[optionIndex], + onValueChange = { pollViewModel.pollOptions[optionIndex] = it }, label = { Text( text = stringResource(R.string.poll_option_index).format(optionIndex), @@ -41,21 +36,23 @@ fun PollOption(optionIndex: Int) { } ) - Button( - modifier = Modifier - .padding(start = 6.dp, top = 2.dp) - .imePadding(), - onClick = { /*TODO*/ }, - 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) - ) + if (optionIndex > 1) { + Button( + modifier = Modifier + .padding(start = 6.dp, top = 2.dp) + .imePadding(), + onClick = { pollViewModel.pollOptions.removeAt(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) + ) + } } } } @@ -63,5 +60,5 @@ fun PollOption(optionIndex: Int) { @Preview @Composable fun PollOptionPreview() { - PollOption(0) + PollOption(NewPollViewModel(), 0) } From 29c4c42547f4fecf8317fe6c74a246637e5fb937 Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Thu, 16 Mar 2023 19:25:54 +0900 Subject: [PATCH 13/36] delete pollOption values on cancel(), move recipients field to optional section, make recipients, options textFields fillMaxWidth --- .../com/vitorpamplona/amethyst/ui/actions/NewPollView.kt | 2 +- .../amethyst/ui/actions/NewPollViewModel.kt | 3 +++ .../vitorpamplona/amethyst/ui/components/PollOption.kt | 9 ++++----- .../amethyst/ui/components/PollRecipientsField.kt | 4 ++++ 4 files changed, 12 insertions(+), 6 deletions(-) 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 3cd3caf28..6e523eaf1 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 @@ -109,7 +109,6 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n } Text(stringResource(R.string.poll_heading_required)) - PollRecipientsField() PollPrimaryDescription(pollViewModel = pollViewModel) pollViewModel.pollOptions.forEachIndexed { index, element -> PollOption(pollViewModel, index) @@ -128,6 +127,7 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n ) } Text(stringResource(R.string.poll_heading_optional)) + PollRecipientsField() PollVoteValueRange() PollConsensusThreshold() PollClosing() 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 index e13bc4ca8..4027994fa 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollViewModel.kt @@ -35,6 +35,9 @@ class NewPollViewModel : NewPostViewModel() { } override fun cancel() { + // delete existing pollOptions + pollOptions = mutableStateListOf("", "") + super.cancel() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt index ab2b1d134..c6132114a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt @@ -2,10 +2,7 @@ package com.vitorpamplona.amethyst.ui.components import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -18,8 +15,10 @@ import com.vitorpamplona.amethyst.ui.actions.NewPollViewModel @Composable fun PollOption(pollViewModel: NewPollViewModel, optionIndex: Int) { - Row() { + Row { OutlinedTextField( + modifier = Modifier + .weight(1F), value = pollViewModel.pollOptions[optionIndex], onValueChange = { pollViewModel.pollOptions[optionIndex] = it }, label = { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollRecipientsField.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollRecipientsField.kt index ed135f09c..6cf29d4a0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollRecipientsField.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollRecipientsField.kt @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst.ui.components +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material.MaterialTheme import androidx.compose.material.OutlinedTextField import androidx.compose.material.Text @@ -8,6 +9,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import com.vitorpamplona.amethyst.R @@ -16,6 +18,8 @@ fun PollRecipientsField() { var text by rememberSaveable() { mutableStateOf("") } OutlinedTextField( + modifier = Modifier + .fillMaxWidth(), value = text, onValueChange = { text = it }, label = { From f0b466071937a6895316d2112f65057045266855 Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Fri, 17 Mar 2023 16:48:26 +0900 Subject: [PATCH 14/36] validate user input for all poll fields, add user's pubkey to recipients field, add poll field saveable texts to pollViewModel, --- .../amethyst/ui/actions/NewPollView.kt | 15 +++-- .../amethyst/ui/actions/NewPollViewModel.kt | 14 +++- .../amethyst/ui/components/PollClosing.kt | 27 +++++++- .../ui/components/PollConsensusThreshold.kt | 27 +++++++- .../amethyst/ui/components/PollOption.kt | 19 +++++- .../ui/components/PollPrimaryDescription.kt | 30 +++++---- .../ui/components/PollRecipientsField.kt | 13 ++-- .../ui/components/PollVoteValueRange.kt | 67 ++++++++++++++++--- app/src/main/res/values/strings.xml | 2 +- 9 files changed, 169 insertions(+), 45 deletions(-) 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 6e523eaf1..07067a2d5 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 @@ -39,6 +39,11 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n val scrollState = rememberScrollState() + // if no recipients, add user's pubkey + if (pollViewModel.zapRecipients.isEmpty()) { + pollViewModel.zapRecipients.add(account.userProfile().pubkeyHex) + } + LaunchedEffect(Unit) { pollViewModel.load(account, baseReplyTo, quote) delay(100) @@ -109,7 +114,8 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n } Text(stringResource(R.string.poll_heading_required)) - PollPrimaryDescription(pollViewModel = pollViewModel) + PollRecipientsField(pollViewModel) + PollPrimaryDescription(pollViewModel) pollViewModel.pollOptions.forEachIndexed { index, element -> PollOption(pollViewModel, index) } @@ -127,10 +133,9 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n ) } Text(stringResource(R.string.poll_heading_optional)) - PollRecipientsField() - PollVoteValueRange() - PollConsensusThreshold() - PollClosing() + PollVoteValueRange(pollViewModel) + PollConsensusThreshold(pollViewModel) + PollClosing(pollViewModel) } } 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 index 4027994fa..c90562fc9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollViewModel.kt @@ -3,12 +3,18 @@ package com.vitorpamplona.amethyst.ui.actions import androidx.compose.runtime.mutableStateListOf import androidx.compose.ui.text.input.TextFieldValue import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User class NewPollViewModel : NewPostViewModel() { + var zapRecipients = mutableStateListOf() var pollOptions = mutableStateListOf("", "") + var zapMax: Int? = null + var zapMin: Int? = null + var consensus: Int? = null + var closedAfter: Int? = null override fun load(account: Account, replyingTo: Note?, quote: Note?) { super.load(account, replyingTo, quote) @@ -32,13 +38,15 @@ class NewPollViewModel : NewPostViewModel() { override fun sendPost() { super.sendPost() + + // delete existing pollOptions + pollOptions = mutableStateListOf("", "") } override fun cancel() { - // delete existing pollOptions - pollOptions = mutableStateListOf("", "") - super.cancel() + + pollOptions = mutableStateListOf("", "") } override fun findUrlInMessage(): String? { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollClosing.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollClosing.kt index 2694e0ff2..c3dd9251c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollClosing.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollClosing.kt @@ -8,22 +8,44 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.MaterialTheme import androidx.compose.material.OutlinedTextField import androidx.compose.material.Text +import androidx.compose.material.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource 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 @Composable -fun PollClosing() { +fun PollClosing(pollViewModel: NewPollViewModel) { var text by rememberSaveable { mutableStateOf("") } + var isInputValid = true + if (text.isNotEmpty()) { + try { + val int = text.toInt() + if (int < 0) { + isInputValid = false + } else { pollViewModel.closedAfter = int } + } catch (e: Exception) { isInputValid = false } + } + + val colorInValid = TextFieldDefaults.outlinedTextFieldColors( + focusedBorderColor = MaterialTheme.colors.error, + unfocusedBorderColor = Color.Red + ) + val colorValid = TextFieldDefaults.outlinedTextFieldColors( + focusedBorderColor = MaterialTheme.colors.primary, + unfocusedBorderColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + Row( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center @@ -33,6 +55,7 @@ fun PollClosing() { onValueChange = { text = it }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), modifier = Modifier.width(150.dp), + colors = if (isInputValid) colorValid else colorInValid, label = { Text( text = stringResource(R.string.poll_closing_time), @@ -52,5 +75,5 @@ fun PollClosing() { @Preview @Composable fun PollClosingPreview() { - PollClosing() + PollClosing(NewPollViewModel()) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollConsensusThreshold.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollConsensusThreshold.kt index 67296bd94..2920d00b9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollConsensusThreshold.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollConsensusThreshold.kt @@ -8,22 +8,44 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.MaterialTheme import androidx.compose.material.OutlinedTextField import androidx.compose.material.Text +import androidx.compose.material.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource 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 @Composable -fun PollConsensusThreshold() { +fun PollConsensusThreshold(pollViewModel: NewPollViewModel) { var text by rememberSaveable { mutableStateOf("") } + var isInputValid = true + if (text.isNotEmpty()) { + try { + val int = text.toInt() + if (int < 0 || int > 100) { + isInputValid = false + } else { pollViewModel.consensus = int } + } catch (e: Exception) { isInputValid = false } + } + + val colorInValid = TextFieldDefaults.outlinedTextFieldColors( + focusedBorderColor = MaterialTheme.colors.error, + unfocusedBorderColor = Color.Red + ) + val colorValid = TextFieldDefaults.outlinedTextFieldColors( + focusedBorderColor = MaterialTheme.colors.primary, + unfocusedBorderColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + Row( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center @@ -33,6 +55,7 @@ fun PollConsensusThreshold() { onValueChange = { text = it }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), modifier = Modifier.width(150.dp), + colors = if (isInputValid) colorValid else colorInValid, label = { Text( text = stringResource(R.string.poll_consensus_threshold), @@ -52,5 +75,5 @@ fun PollConsensusThreshold() { @Preview @Composable fun PollConsensusThresholdPreview() { - PollConsensusThreshold() + PollConsensusThreshold(NewPollViewModel()) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt index c6132114a..df9533e9a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.material.* 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.tooling.preview.Preview @@ -15,6 +16,20 @@ import com.vitorpamplona.amethyst.ui.actions.NewPollViewModel @Composable fun PollOption(pollViewModel: NewPollViewModel, optionIndex: Int) { + var isInputValid = true + if (pollViewModel.pollOptions[optionIndex].isEmpty()) { + isInputValid = false + } + + val colorInValid = TextFieldDefaults.outlinedTextFieldColors( + focusedBorderColor = MaterialTheme.colors.error, + unfocusedBorderColor = Color.Red + ) + val colorValid = TextFieldDefaults.outlinedTextFieldColors( + focusedBorderColor = MaterialTheme.colors.primary, + unfocusedBorderColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + Row { OutlinedTextField( modifier = Modifier @@ -32,8 +47,8 @@ fun PollOption(pollViewModel: NewPollViewModel, optionIndex: Int) { text = stringResource(R.string.poll_option_description), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) - } - + }, + colors = if (isInputValid) colorValid else colorInValid ) if (optionIndex > 1) { Button( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollPrimaryDescription.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollPrimaryDescription.kt index 6f65c8266..e68896f40 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollPrimaryDescription.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollPrimaryDescription.kt @@ -1,13 +1,10 @@ package com.vitorpamplona.amethyst.ui.components -import androidx.compose.foundation.border import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -30,6 +27,20 @@ fun PollPrimaryDescription(pollViewModel: NewPollViewModel) { val focusRequester = remember { FocusRequester() } val keyboardController = LocalSoftwareKeyboardController.current + var isInputValid = true + if (pollViewModel.message.text.isEmpty()) { + isInputValid = false + } + + val colorInValid = TextFieldDefaults.outlinedTextFieldColors( + focusedBorderColor = MaterialTheme.colors.error, + unfocusedBorderColor = Color.Red + ) + val colorValid = TextFieldDefaults.outlinedTextFieldColors( + focusedBorderColor = MaterialTheme.colors.primary, + unfocusedBorderColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + OutlinedTextField( value = pollViewModel.message, onValueChange = { @@ -47,11 +58,6 @@ fun PollPrimaryDescription(pollViewModel: NewPollViewModel) { modifier = Modifier .fillMaxWidth() .padding(top = 8.dp) - .border( - width = 1.dp, - color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), - shape = RoundedCornerShape(8.dp) - ) .focusRequester(focusRequester) .onFocusChanged { if (it.isFocused) { @@ -64,11 +70,7 @@ fun PollPrimaryDescription(pollViewModel: NewPollViewModel) { color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) }, - colors = TextFieldDefaults - .outlinedTextFieldColors( - unfocusedBorderColor = Color.Transparent, - focusedBorderColor = Color.Transparent - ), + colors = if (isInputValid) colorValid else colorInValid, visualTransformation = UrlUserTagTransformation(MaterialTheme.colors.primary), textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content) ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollRecipientsField.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollRecipientsField.kt index 6cf29d4a0..131821727 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollRecipientsField.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollRecipientsField.kt @@ -5,23 +5,20 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.OutlinedTextField import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.actions.NewPollViewModel @Composable -fun PollRecipientsField() { - var text by rememberSaveable() { mutableStateOf("") } +fun PollRecipientsField(pollViewModel: NewPollViewModel) { OutlinedTextField( modifier = Modifier .fillMaxWidth(), - value = text, - onValueChange = { text = it }, + value = pollViewModel.zapRecipients[0], + onValueChange = { /* TODO */ }, + enabled = false, // TODO enable add recipients label = { Text( text = stringResource(R.string.poll_zap_recipients), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollVoteValueRange.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollVoteValueRange.kt index 63d8881eb..9e9160256 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollVoteValueRange.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollVoteValueRange.kt @@ -8,32 +8,82 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.MaterialTheme import androidx.compose.material.OutlinedTextField import androidx.compose.material.Text +import androidx.compose.material.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource 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 @Composable -fun PollVoteValueRange() { - var minText by rememberSaveable { mutableStateOf("") } - var maxText by rememberSaveable { mutableStateOf("") } +fun PollVoteValueRange(pollViewModel: NewPollViewModel) { + var textMax by rememberSaveable { mutableStateOf("") } + var textMin by rememberSaveable { mutableStateOf("") } + + // check for zapMax amounts < 1 + var isMaxValid = true + if (textMax.isNotEmpty()) { + try { + val int = textMax.toInt() + if ( int < 1) + isMaxValid = false + else pollViewModel.zapMax = int + } catch (e: Exception) { isMaxValid = false } + } + + // check for minZap amounts < 1 + var isMinValid = true + if (textMin.isNotEmpty()) { + try { + val int = textMin.toInt() + if ( int < 1) + isMinValid = false + else pollViewModel.zapMin = int + } catch (e: Exception) { isMinValid = false } + } + + // check for zapMin > zapMax + if (textMin.isNotEmpty() && textMax.isNotEmpty()) { + try { + val intMin = textMin.toInt() + val intMax = textMax.toInt() + + if ( intMin > intMax) { + isMinValid = false + isMaxValid = false + } + } catch (e: Exception) { + isMinValid = false + isMaxValid = false + } + } + + val colorInValid = TextFieldDefaults.outlinedTextFieldColors( + focusedBorderColor = MaterialTheme.colors.error, + unfocusedBorderColor = Color.Red) + val colorValid = TextFieldDefaults.outlinedTextFieldColors( + focusedBorderColor = MaterialTheme.colors.primary, + unfocusedBorderColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) Row( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center ) { OutlinedTextField( - value = minText, - onValueChange = { minText = it }, + value = textMin, + onValueChange = { textMin = it }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), modifier = Modifier.width(150.dp), + colors = if (isMinValid) colorValid else colorInValid, label = { Text( text = stringResource(R.string.poll_zap_value_min), @@ -48,10 +98,11 @@ fun PollVoteValueRange() { } ) OutlinedTextField( - value = maxText, - onValueChange = { maxText = it }, + value = textMax, + onValueChange = { textMax = it }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), modifier = Modifier.width(150.dp), + colors = if (isMaxValid) colorValid else colorInValid, label = { Text( text = stringResource(R.string.poll_zap_value_max), @@ -71,5 +122,5 @@ fun PollVoteValueRange() { @Preview @Composable fun PollVoteValueRangePreview() { - PollVoteValueRange() + PollVoteValueRange(NewPollViewModel()) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bba5e2191..2a5c1bdfd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -230,7 +230,7 @@ Zap minimum Zap maximum Consensus - % + (0–100)% sats Close after days From 551ed64e98b6c0549911bc1ca6e33dbf070e16c1 Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Fri, 17 Mar 2023 16:57:17 +0900 Subject: [PATCH 15/36] add PollNoteEvent type --- .../amethyst/service/model/PollNoteEvent.kt | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt new file mode 100644 index 000000000..9c910477d --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt @@ -0,0 +1,76 @@ +package com.vitorpamplona.amethyst.service.model + +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey +import nostr.postr.Utils +import java.util.Date + +class PollNoteEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey +) : Event(id, pubKey, createdAt, kind, tags, content, sig) { + fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } + fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { + val aTagValue = it.getOrNull(1) + val relay = it.getOrNull(2) + + if (aTagValue != null) ATag.parse(aTagValue, relay) else null + } + + fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } + + companion object { + const val kind = 6969 + + fun create(msg: String, + replyTos: List?, + mentions: List?, + addresses: List?, + privateKey: ByteArray, + createdAt: Long = Date().time / 1000): PollNoteEvent { + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() + val tags = mutableListOf>() + replyTos?.forEach { + tags.add(listOf("e", it)) + } + mentions?.forEach { + tags.add(listOf("p", it)) + } + addresses?.forEach { + tags.add(listOf("a", it.toTag())) + } + val id = generateId(pubKey, createdAt, kind, tags, msg) + val sig = Utils.sign(id, privateKey) + return PollNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey()) + } + } +} + +/* +{ + "id": <32-bytes lowercase hex-encoded sha256 of the serialized event data> + "pubkey": <32-bytes lowercase hex-encoded public key of the event creator>, + "created_at": , + "kind": 6969, + "tags": [ + ["e", <32-bytes hex of the id of the poll event>, ], + ["p", <32-bytes hex of the key>, ], + ["poll_options", + "[[0, 'poll option 0 description string'], + [1, 'poll option 1 description string'], + [, 'poll option description string']]" + ], + ["value_maximum", "maximum satoshi value for inclusion in tally"], + ["value_minimum", "minimum satoshi value for inclusion in tally"], + ["consensus_threshold", "required percentage to attain consensus <0..100>"], + ["closed_at", "unix timestamp in seconds"], + ], + "ots": + "content": , + "sig": <64-bytes hex of the signature of the sha256 hash of the serialized event data, which is the same as the "id" field> +} + */ From 7a53708cccde0a4d155d52fe1841d9729af44177 Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Sat, 18 Mar 2023 15:38:58 +0900 Subject: [PATCH 16/36] add fields to pollNoteEvent, add clearState fun to pollViewModel, add refined PollNoteEvent to Event, --- .../amethyst/service/model/Event.kt | 1 + .../amethyst/service/model/PollNoteEvent.kt | 39 ++++++++++++++++--- .../amethyst/ui/actions/NewPollView.kt | 7 +--- .../amethyst/ui/actions/NewPollViewModel.kt | 15 +++++-- .../ui/components/PollRecipientsField.kt | 7 +++- 5 files changed, 53 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt index 70b237e2d..e83d1eb5d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt @@ -178,6 +178,7 @@ open class Event( LnZapRequestEvent.kind -> LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig) LongTextNoteEvent.kind -> LongTextNoteEvent(id, pubKey, createdAt, tags, content, sig) MetadataEvent.kind -> MetadataEvent(id, pubKey, createdAt, tags, content, sig) + PollNoteEvent.kind -> PollNoteEvent(id, pubKey, createdAt, tags, content, sig) PrivateDmEvent.kind -> PrivateDmEvent(id, pubKey, createdAt, tags, content, sig) ReactionEvent.kind -> ReactionEvent(id, pubKey, createdAt, tags, content, sig) RecommendRelayEvent.kind -> RecommendRelayEvent(id, pubKey, createdAt, tags, content, sig, lenient) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt index 9c910477d..6cb15d4a9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt @@ -5,11 +5,18 @@ import com.vitorpamplona.amethyst.model.toHexKey import nostr.postr.Utils import java.util.Date +const val POLL_OPTIONS = "poll_options" +const val VALUE_MAXIMUM = "value_maximum" +const val VALUE_MINIMUM = "value_minimum" +const val CONSENSUS_THRESHOLD = "consensus_threshold" +const val CLOSED_AT = "closed_at" + class PollNoteEvent( id: HexKey, pubKey: HexKey, createdAt: Long, tags: List>, + // ots: , TODO implement OTS: https://github.com/opentimestamps/java-opentimestamps content: String, sig: HexKey ) : Event(id, pubKey, createdAt, kind, tags, content, sig) { @@ -23,15 +30,28 @@ class PollNoteEvent( fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } + fun pollOptions() = tags.filter { it.firstOrNull() == POLL_OPTIONS }.mapNotNull { it.getOrNull(1) } + fun valueMaximum() = tags.filter { it.firstOrNull() == VALUE_MAXIMUM }.mapNotNull { it.getOrNull(1) } + fun valueMinimum() = tags.filter { it.firstOrNull() == VALUE_MINIMUM }.mapNotNull { it.getOrNull(1) } + fun consensusThreshold() = tags.filter { it.firstOrNull() == CONSENSUS_THRESHOLD }.mapNotNull { it.getOrNull(1) } + fun closedAt() = tags.filter { it.firstOrNull() == CLOSED_AT }.mapNotNull { it.getOrNull(1) } + companion object { const val kind = 6969 - fun create(msg: String, - replyTos: List?, - mentions: List?, - addresses: List?, - privateKey: ByteArray, - createdAt: Long = Date().time / 1000): PollNoteEvent { + fun create( + msg: String, + replyTos: List?, + mentions: List?, + addresses: List?, + privateKey: ByteArray, + createdAt: Long = Date().time / 1000, + pollOptions: List>, + valueMaximum: Int?, + valueMinimum: Int?, + consensusThreshold: Int?, + closedAt: Int? + ): PollNoteEvent { val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = mutableListOf>() replyTos?.forEach { @@ -43,6 +63,13 @@ class PollNoteEvent( addresses?.forEach { tags.add(listOf("a", it.toTag())) } + pollOptions.forEach { + tags.add(listOf(POLL_OPTIONS, it.toString())) + } + tags.add(listOf(VALUE_MAXIMUM, valueMaximum.toString())) + tags.add(listOf(VALUE_MINIMUM, valueMinimum.toString())) + tags.add(listOf(CONSENSUS_THRESHOLD, consensusThreshold.toString())) + tags.add(listOf(CLOSED_AT, closedAt.toString())) val id = generateId(pubKey, createdAt, kind, tags, msg) val sig = Utils.sign(id, privateKey) return PollNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey()) 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 07067a2d5..ee54ae23b 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 @@ -39,11 +39,6 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n val scrollState = rememberScrollState() - // if no recipients, add user's pubkey - if (pollViewModel.zapRecipients.isEmpty()) { - pollViewModel.zapRecipients.add(account.userProfile().pubkeyHex) - } - LaunchedEffect(Unit) { pollViewModel.load(account, baseReplyTo, quote) delay(100) @@ -114,7 +109,7 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n } Text(stringResource(R.string.poll_heading_required)) - PollRecipientsField(pollViewModel) + PollRecipientsField(pollViewModel, account) PollPrimaryDescription(pollViewModel) pollViewModel.pollOptions.forEachIndexed { index, element -> PollOption(pollViewModel, index) 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 index c90562fc9..e54c2107f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollViewModel.kt @@ -39,14 +39,13 @@ class NewPollViewModel : NewPostViewModel() { override fun sendPost() { super.sendPost() - // delete existing pollOptions - pollOptions = mutableStateListOf("", "") + clearStates() } override fun cancel() { super.cancel() - pollOptions = mutableStateListOf("", "") + clearStates() } override fun findUrlInMessage(): String? { @@ -64,4 +63,14 @@ class NewPollViewModel : NewPostViewModel() { override fun autocompleteWithUser(item: User) { super.autocompleteWithUser(item) } + + private fun clearStates() { + // clear states + zapRecipients = mutableStateListOf() + pollOptions = mutableStateListOf("", "") + zapMax = null + zapMin = null + consensus = null + closedAfter = null + } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollRecipientsField.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollRecipientsField.kt index 131821727..602a1c1af 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollRecipientsField.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollRecipientsField.kt @@ -8,10 +8,15 @@ import androidx.compose.runtime.Composable 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 @Composable -fun PollRecipientsField(pollViewModel: NewPollViewModel) { +fun PollRecipientsField(pollViewModel: NewPollViewModel, account: Account) { + // if no recipients, add user's pubkey + if (pollViewModel.zapRecipients.isEmpty()) { + pollViewModel.zapRecipients.add(account.userProfile().pubkeyHex) + } OutlinedTextField( modifier = Modifier From 7890ac9db5c43f476f46418d790b1388edfb33a6 Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Sun, 19 Mar 2023 12:43:15 +0900 Subject: [PATCH 17/36] create pollSend funs, --- .../vitorpamplona/amethyst/model/Account.kt | 48 +++++++--- .../amethyst/service/model/PollNoteEvent.kt | 6 +- .../amethyst/ui/actions/NewPollView.kt | 2 +- .../amethyst/ui/actions/NewPollViewModel.kt | 96 +++++++++++++++---- .../amethyst/ui/actions/NewPostViewModel.kt | 2 +- .../amethyst/ui/components/PollClosing.kt | 2 +- .../ui/components/PollConsensusThreshold.kt | 2 +- .../ui/components/PollVoteValueRange.kt | 13 +-- 8 files changed, 125 insertions(+), 46 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 9645fc180..8b0c9502d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -3,20 +3,7 @@ package com.vitorpamplona.amethyst.model import android.content.res.Resources import androidx.core.os.ConfigurationCompat import androidx.lifecycle.LiveData -import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent -import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent -import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent -import com.vitorpamplona.amethyst.service.model.Contact -import com.vitorpamplona.amethyst.service.model.ContactListEvent -import com.vitorpamplona.amethyst.service.model.DeletionEvent -import com.vitorpamplona.amethyst.service.model.IdentityClaim -import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent -import com.vitorpamplona.amethyst.service.model.MetadataEvent -import com.vitorpamplona.amethyst.service.model.PrivateDmEvent -import com.vitorpamplona.amethyst.service.model.ReactionEvent -import com.vitorpamplona.amethyst.service.model.ReportEvent -import com.vitorpamplona.amethyst.service.model.RepostEvent -import com.vitorpamplona.amethyst.service.model.TextNoteEvent +import com.vitorpamplona.amethyst.service.model.* import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.Constants import com.vitorpamplona.amethyst.service.relays.FeedType @@ -300,6 +287,39 @@ class Account( LocalCache.consume(signedEvent) } + fun sendPoll( + message: String, + replyTo: List?, + mentions: List?, + pollOptions: List>, + valueMaximum: Int?, + valueMinimum: Int?, + consensusThreshold: Int?, + closedAt: Int? + ) { + if (!isWriteable()) return + + val repliesToHex = replyTo?.map { it.idHex } + val mentionsHex = mentions?.map { it.pubkeyHex } + val addresses = replyTo?.mapNotNull { it.address() } + + val signedEvent = PollNoteEvent.create( + msg = message, + replyTos = repliesToHex, + mentions = mentionsHex, + addresses = addresses, + privateKey = loggedIn.privKey!!, + pollOptions = pollOptions, + valueMaximum = valueMaximum, + valueMinimum = valueMinimum, + consensusThreshold = consensusThreshold, + closedAt = closedAt + ) + println("PollNoteEvent: %s".format(signedEvent.toJson())) + // Client.send(signedEvent) + // LocalCache.consume(signedEvent) + } + fun sendChannelMessage(message: String, toChannel: String, replyingTo: Note? = null, mentions: List?) { if (!isWriteable()) return diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt index 6cb15d4a9..f6409aaac 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt @@ -16,7 +16,7 @@ class PollNoteEvent( pubKey: HexKey, createdAt: Long, tags: List>, - // ots: , TODO implement OTS: https://github.com/opentimestamps/java-opentimestamps + // ots: String?, TODO implement OTS: https://github.com/opentimestamps/java-opentimestamps content: String, sig: HexKey ) : Event(id, pubKey, createdAt, kind, tags, content, sig) { @@ -63,9 +63,7 @@ class PollNoteEvent( addresses?.forEach { tags.add(listOf("a", it.toTag())) } - pollOptions.forEach { - tags.add(listOf(POLL_OPTIONS, it.toString())) - } + tags.add(listOf(POLL_OPTIONS, pollOptions.toString())) tags.add(listOf(VALUE_MAXIMUM, valueMaximum.toString())) tags.add(listOf(VALUE_MINIMUM, valueMinimum.toString())) tags.add(listOf(CONSENSUS_THRESHOLD, consensusThreshold.toString())) 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 ee54ae23b..c3c768b31 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 @@ -84,7 +84,7 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n PollButton( onPost = { - pollViewModel.sendPost() + pollViewModel.sendPoll() 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 index e54c2107f..2259cc83e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollViewModel.kt @@ -2,19 +2,17 @@ package com.vitorpamplona.amethyst.ui.actions import androidx.compose.runtime.mutableStateListOf import androidx.compose.ui.text.input.TextFieldValue -import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.HexKey -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.* +import com.vitorpamplona.amethyst.service.nip19.Nip19 class NewPollViewModel : NewPostViewModel() { var zapRecipients = mutableStateListOf() var pollOptions = mutableStateListOf("", "") - var zapMax: Int? = null - var zapMin: Int? = null - var consensus: Int? = null - var closedAfter: Int? = null + var valueMaximum: Int? = null + var valueMinimum: Int? = null + var consensusThreshold: Int? = null + var closedAt: Int? = null override fun load(account: Account, replyingTo: Note?, quote: Note?) { super.load(account, replyingTo, quote) @@ -36,16 +34,65 @@ class NewPollViewModel : NewPostViewModel() { return super.tagIndex(note) } - override fun sendPost() { - super.sendPost() + 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) - clearStates() + 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, getPollOptionsList(), valueMaximum, valueMinimum, consensusThreshold, closedAt) + + clearPollStates() } override fun cancel() { super.cancel() - clearStates() + clearPollStates() } override fun findUrlInMessage(): String? { @@ -64,13 +111,26 @@ class NewPollViewModel : NewPostViewModel() { super.autocompleteWithUser(item) } - private fun clearStates() { - // clear states + // clear all states + private fun clearPollStates() { + message = TextFieldValue("") + urlPreview = null + isUploadingImage = false + mentions = null + zapRecipients = mutableStateListOf() pollOptions = mutableStateListOf("", "") - zapMax = null - zapMin = null - consensus = null - closedAfter = null + valueMaximum = null + valueMinimum = null + consensusThreshold = null + closedAt = null + } + + private fun getPollOptionsList(): List> { + val optionsList: MutableList> = mutableListOf() + pollOptions.forEachIndexed { i, s -> + optionsList.add(mapOf(Pair(i, s))) + } + return optionsList } } 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 98feff723..d80a5b07b 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 @@ -71,7 +71,7 @@ open class NewPostViewModel : ViewModel() { return (if (originalNote?.channel() != null) 1 else 0) + (replyTos?.indexOf(note) ?: 0) } - open fun sendPost() { + fun sendPost() { // adds all references to mentions and reply tos message.text.split('\n').forEach { paragraph: String -> paragraph.split(' ').forEach { word: String -> diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollClosing.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollClosing.kt index c3dd9251c..9d4e45f3f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollClosing.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollClosing.kt @@ -33,7 +33,7 @@ fun PollClosing(pollViewModel: NewPollViewModel) { val int = text.toInt() if (int < 0) { isInputValid = false - } else { pollViewModel.closedAfter = int } + } else { pollViewModel.closedAt = int } } catch (e: Exception) { isInputValid = false } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollConsensusThreshold.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollConsensusThreshold.kt index 2920d00b9..f60a11e77 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollConsensusThreshold.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollConsensusThreshold.kt @@ -33,7 +33,7 @@ fun PollConsensusThreshold(pollViewModel: NewPollViewModel) { val int = text.toInt() if (int < 0 || int > 100) { isInputValid = false - } else { pollViewModel.consensus = int } + } else { pollViewModel.consensusThreshold = int } } catch (e: Exception) { isInputValid = false } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollVoteValueRange.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollVoteValueRange.kt index 9e9160256..fa6f93c62 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollVoteValueRange.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollVoteValueRange.kt @@ -33,9 +33,9 @@ fun PollVoteValueRange(pollViewModel: NewPollViewModel) { if (textMax.isNotEmpty()) { try { val int = textMax.toInt() - if ( int < 1) + if (int < 1) { isMaxValid = false - else pollViewModel.zapMax = int + } else { pollViewModel.valueMaximum = int } } catch (e: Exception) { isMaxValid = false } } @@ -44,9 +44,9 @@ fun PollVoteValueRange(pollViewModel: NewPollViewModel) { if (textMin.isNotEmpty()) { try { val int = textMin.toInt() - if ( int < 1) + if (int < 1) { isMinValid = false - else pollViewModel.zapMin = int + } else { pollViewModel.valueMinimum = int } } catch (e: Exception) { isMinValid = false } } @@ -56,7 +56,7 @@ fun PollVoteValueRange(pollViewModel: NewPollViewModel) { val intMin = textMin.toInt() val intMax = textMax.toInt() - if ( intMin > intMax) { + if (intMin > intMax) { isMinValid = false isMaxValid = false } @@ -68,7 +68,8 @@ fun PollVoteValueRange(pollViewModel: NewPollViewModel) { val colorInValid = TextFieldDefaults.outlinedTextFieldColors( focusedBorderColor = MaterialTheme.colors.error, - unfocusedBorderColor = Color.Red) + unfocusedBorderColor = Color.Red + ) val colorValid = TextFieldDefaults.outlinedTextFieldColors( focusedBorderColor = MaterialTheme.colors.primary, unfocusedBorderColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) From 0366f6235c1fce71cea44f9ba5884605239dc058 Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Sun, 19 Mar 2023 14:08:37 +0900 Subject: [PATCH 18/36] move valid input flags to pollViewModel, disable sendPoll button unless all fields valid --- .../amethyst/ui/actions/NewPollView.kt | 7 +++++- .../amethyst/ui/actions/NewPollViewModel.kt | 7 ++++++ .../amethyst/ui/components/PollClosing.kt | 8 +++---- .../ui/components/PollConsensusThreshold.kt | 8 +++---- .../amethyst/ui/components/PollOption.kt | 7 +----- .../ui/components/PollRecipientsField.kt | 2 ++ .../ui/components/PollVoteValueRange.kt | 24 +++++++++---------- 7 files changed, 36 insertions(+), 27 deletions(-) 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 c3c768b31..22fa9d3ef 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 @@ -88,7 +88,12 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n onClose() }, isActive = pollViewModel.message.text.isNotBlank() && - !pollViewModel.isUploadingImage + pollViewModel.pollOptions.all { it.isNotEmpty() } && + pollViewModel.isValidRecipients.value && + pollViewModel.isValidvalueMaximum.value && + pollViewModel.isValidvalueMinimum.value && + pollViewModel.isValidConsensusThreshold.value && + pollViewModel.isValidClosedAt.value ) } 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 index 2259cc83e..5b5b637e9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollViewModel.kt @@ -1,6 +1,7 @@ package com.vitorpamplona.amethyst.ui.actions import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.text.input.TextFieldValue import com.vitorpamplona.amethyst.model.* import com.vitorpamplona.amethyst.service.nip19.Nip19 @@ -14,6 +15,12 @@ class NewPollViewModel : NewPostViewModel() { 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) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollClosing.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollClosing.kt index 9d4e45f3f..c35b5faa7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollClosing.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollClosing.kt @@ -27,14 +27,14 @@ import com.vitorpamplona.amethyst.ui.actions.NewPollViewModel fun PollClosing(pollViewModel: NewPollViewModel) { var text by rememberSaveable { mutableStateOf("") } - var isInputValid = true + pollViewModel.isValidClosedAt.value = true if (text.isNotEmpty()) { try { val int = text.toInt() if (int < 0) { - isInputValid = false + pollViewModel.isValidClosedAt.value = false } else { pollViewModel.closedAt = int } - } catch (e: Exception) { isInputValid = false } + } catch (e: Exception) { pollViewModel.isValidClosedAt.value = false } } val colorInValid = TextFieldDefaults.outlinedTextFieldColors( @@ -55,7 +55,7 @@ fun PollClosing(pollViewModel: NewPollViewModel) { onValueChange = { text = it }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), modifier = Modifier.width(150.dp), - colors = if (isInputValid) colorValid else colorInValid, + colors = if (pollViewModel.isValidClosedAt.value) colorValid else colorInValid, label = { Text( text = stringResource(R.string.poll_closing_time), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollConsensusThreshold.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollConsensusThreshold.kt index f60a11e77..bb8e3d42a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollConsensusThreshold.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollConsensusThreshold.kt @@ -27,14 +27,14 @@ import com.vitorpamplona.amethyst.ui.actions.NewPollViewModel fun PollConsensusThreshold(pollViewModel: NewPollViewModel) { var text by rememberSaveable { mutableStateOf("") } - var isInputValid = true + pollViewModel.isValidConsensusThreshold.value = true if (text.isNotEmpty()) { try { val int = text.toInt() if (int < 0 || int > 100) { - isInputValid = false + pollViewModel.isValidConsensusThreshold.value = false } else { pollViewModel.consensusThreshold = int } - } catch (e: Exception) { isInputValid = false } + } catch (e: Exception) { pollViewModel.isValidConsensusThreshold.value = false } } val colorInValid = TextFieldDefaults.outlinedTextFieldColors( @@ -55,7 +55,7 @@ fun PollConsensusThreshold(pollViewModel: NewPollViewModel) { onValueChange = { text = it }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), modifier = Modifier.width(150.dp), - colors = if (isInputValid) colorValid else colorInValid, + colors = if (pollViewModel.isValidConsensusThreshold.value) colorValid else colorInValid, label = { Text( text = stringResource(R.string.poll_consensus_threshold), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt index df9533e9a..441c71142 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt @@ -16,11 +16,6 @@ import com.vitorpamplona.amethyst.ui.actions.NewPollViewModel @Composable fun PollOption(pollViewModel: NewPollViewModel, optionIndex: Int) { - var isInputValid = true - if (pollViewModel.pollOptions[optionIndex].isEmpty()) { - isInputValid = false - } - val colorInValid = TextFieldDefaults.outlinedTextFieldColors( focusedBorderColor = MaterialTheme.colors.error, unfocusedBorderColor = Color.Red @@ -48,7 +43,7 @@ fun PollOption(pollViewModel: NewPollViewModel, optionIndex: Int) { color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) }, - colors = if (isInputValid) colorValid else colorInValid + colors = if (pollViewModel.pollOptions[optionIndex].isNotEmpty()) colorValid else colorInValid ) if (optionIndex > 1) { Button( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollRecipientsField.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollRecipientsField.kt index 602a1c1af..4498d2ad6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollRecipientsField.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollRecipientsField.kt @@ -18,6 +18,8 @@ fun PollRecipientsField(pollViewModel: NewPollViewModel, account: Account) { pollViewModel.zapRecipients.add(account.userProfile().pubkeyHex) } + // TODO allow add multiple recipients and check input validity + OutlinedTextField( modifier = Modifier .fillMaxWidth(), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollVoteValueRange.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollVoteValueRange.kt index fa6f93c62..598290bf7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollVoteValueRange.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollVoteValueRange.kt @@ -29,25 +29,25 @@ fun PollVoteValueRange(pollViewModel: NewPollViewModel) { var textMin by rememberSaveable { mutableStateOf("") } // check for zapMax amounts < 1 - var isMaxValid = true + pollViewModel.isValidvalueMaximum.value = true if (textMax.isNotEmpty()) { try { val int = textMax.toInt() if (int < 1) { - isMaxValid = false + pollViewModel.isValidvalueMaximum.value = false } else { pollViewModel.valueMaximum = int } - } catch (e: Exception) { isMaxValid = false } + } catch (e: Exception) { pollViewModel.isValidvalueMaximum.value = false } } // check for minZap amounts < 1 - var isMinValid = true + pollViewModel.isValidvalueMinimum.value = true if (textMin.isNotEmpty()) { try { val int = textMin.toInt() if (int < 1) { - isMinValid = false + pollViewModel.isValidvalueMinimum.value = false } else { pollViewModel.valueMinimum = int } - } catch (e: Exception) { isMinValid = false } + } catch (e: Exception) { pollViewModel.isValidvalueMinimum.value = false } } // check for zapMin > zapMax @@ -57,12 +57,12 @@ fun PollVoteValueRange(pollViewModel: NewPollViewModel) { val intMax = textMax.toInt() if (intMin > intMax) { - isMinValid = false - isMaxValid = false + pollViewModel.isValidvalueMinimum.value = false + pollViewModel.isValidvalueMaximum.value = false } } catch (e: Exception) { - isMinValid = false - isMaxValid = false + pollViewModel.isValidvalueMinimum.value = false + pollViewModel.isValidvalueMaximum.value = false } } @@ -84,7 +84,7 @@ fun PollVoteValueRange(pollViewModel: NewPollViewModel) { onValueChange = { textMin = it }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), modifier = Modifier.width(150.dp), - colors = if (isMinValid) colorValid else colorInValid, + colors = if (pollViewModel.isValidvalueMinimum.value) colorValid else colorInValid, label = { Text( text = stringResource(R.string.poll_zap_value_min), @@ -103,7 +103,7 @@ fun PollVoteValueRange(pollViewModel: NewPollViewModel) { onValueChange = { textMax = it }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), modifier = Modifier.width(150.dp), - colors = if (isMaxValid) colorValid else colorInValid, + colors = if (pollViewModel.isValidvalueMaximum.value) colorValid else colorInValid, label = { Text( text = stringResource(R.string.poll_zap_value_max), From 8171c1ee5eee381e55ce63273d7b90d86464627e Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Sun, 19 Mar 2023 22:15:00 +0900 Subject: [PATCH 19/36] change pollOptions to type Map, remove always true condition from Account.isAcceptable() --- .../vitorpamplona/amethyst/model/Account.kt | 21 ++++------------ .../amethyst/service/model/PollNoteEvent.kt | 17 ++++++++----- .../amethyst/ui/actions/NewPollView.kt | 6 ++--- .../amethyst/ui/actions/NewPollViewModel.kt | 25 +++++++++++-------- .../amethyst/ui/components/PollOption.kt | 6 ++--- 5 files changed, 36 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 8b0c9502d..5276a14cd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -4,21 +4,10 @@ import android.content.res.Resources import androidx.core.os.ConfigurationCompat import androidx.lifecycle.LiveData import com.vitorpamplona.amethyst.service.model.* -import com.vitorpamplona.amethyst.service.relays.Client -import com.vitorpamplona.amethyst.service.relays.Constants -import com.vitorpamplona.amethyst.service.relays.FeedType -import com.vitorpamplona.amethyst.service.relays.Relay -import com.vitorpamplona.amethyst.service.relays.RelayPool -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import com.vitorpamplona.amethyst.service.relays.* +import kotlinx.coroutines.* import nostr.postr.Persona -import java.util.Locale +import java.util.* import java.util.concurrent.atomic.AtomicBoolean val DefaultChannels = setOf( @@ -291,7 +280,7 @@ class Account( message: String, replyTo: List?, mentions: List?, - pollOptions: List>, + pollOptions: Map, valueMaximum: Int?, valueMinimum: Int?, consensusThreshold: Int?, @@ -535,7 +524,7 @@ class Account( isAcceptableDirect(note) && ( note.event !is RepostEvent || - (note.event is RepostEvent && note.replyTo?.firstOrNull { isAcceptableDirect(it) } != null) + (note.replyTo?.firstOrNull { isAcceptableDirect(it) } != null) ) // is not a reaction about a blocked post } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt index f6409aaac..91215db12 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt @@ -46,7 +46,7 @@ class PollNoteEvent( addresses: List?, privateKey: ByteArray, createdAt: Long = Date().time / 1000, - pollOptions: List>, + pollOptions: Map, valueMaximum: Int?, valueMinimum: Int?, consensusThreshold: Int?, @@ -63,7 +63,7 @@ class PollNoteEvent( addresses?.forEach { tags.add(listOf("a", it.toTag())) } - tags.add(listOf(POLL_OPTIONS, pollOptions.toString())) + tags.add(listOf(POLL_OPTIONS, gson.toJson(pollOptions))) tags.add(listOf(VALUE_MAXIMUM, valueMaximum.toString())) tags.add(listOf(VALUE_MINIMUM, valueMinimum.toString())) tags.add(listOf(CONSENSUS_THRESHOLD, consensusThreshold.toString())) @@ -72,6 +72,10 @@ class PollNoteEvent( val sig = Utils.sign(id, privateKey) return PollNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey()) } + + fun parseJsonPollOptions(s: String): Map { + return gson.fromJson>(s, MutableMap::class.java) + } } } @@ -84,10 +88,11 @@ class PollNoteEvent( "tags": [ ["e", <32-bytes hex of the id of the poll event>, ], ["p", <32-bytes hex of the key>, ], - ["poll_options", - "[[0, 'poll option 0 description string'], - [1, 'poll option 1 description string'], - [, 'poll option description string']]" + ["poll_options", "{ + \"0\": \"poll option 0 description string\", + \"1\": \"poll option 1 description string\", + \"n\": \"poll option description string\" + }" ], ["value_maximum", "maximum satoshi value for inclusion in tally"], ["value_minimum", "minimum satoshi value for inclusion in tally"], 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 22fa9d3ef..ddab2e4fa 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 @@ -88,7 +88,7 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n onClose() }, isActive = pollViewModel.message.text.isNotBlank() && - pollViewModel.pollOptions.all { it.isNotEmpty() } && + pollViewModel.pollOptions.values.all { it.isNotEmpty() } && pollViewModel.isValidRecipients.value && pollViewModel.isValidvalueMaximum.value && pollViewModel.isValidvalueMinimum.value && @@ -116,11 +116,11 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n Text(stringResource(R.string.poll_heading_required)) PollRecipientsField(pollViewModel, account) PollPrimaryDescription(pollViewModel) - pollViewModel.pollOptions.forEachIndexed { index, element -> + pollViewModel.pollOptions.values.forEachIndexed { index, element -> PollOption(pollViewModel, index) } Button( - onClick = { pollViewModel.pollOptions.add("") }, + onClick = { pollViewModel.pollOptions.values.add("") }, border = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f)), colors = ButtonDefaults.outlinedButtonColors( contentColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) 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 index 5b5b637e9..3119d65c4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollViewModel.kt @@ -1,15 +1,19 @@ package com.vitorpamplona.amethyst.ui.actions +import androidx.annotation.Keep import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.text.input.TextFieldValue +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken import com.vitorpamplona.amethyst.model.* import com.vitorpamplona.amethyst.service.nip19.Nip19 class NewPollViewModel : NewPostViewModel() { var zapRecipients = mutableStateListOf() - var pollOptions = mutableStateListOf("", "") + var pollOptions = mutableStateMapOf(Pair(0, ""), Pair(1, "")) var valueMaximum: Int? = null var valueMinimum: Int? = null var consensusThreshold: Int? = null @@ -91,7 +95,7 @@ class NewPollViewModel : NewPostViewModel() { account?.sendPoll(newMessage, replyTos, mentions) }*/ - account?.sendPoll(newMessage, replyTos, mentions, getPollOptionsList(), valueMaximum, valueMinimum, consensusThreshold, closedAt) + account?.sendPoll(newMessage, replyTos, mentions, pollOptions, valueMaximum, valueMinimum, consensusThreshold, closedAt) clearPollStates() } @@ -126,18 +130,17 @@ class NewPollViewModel : NewPostViewModel() { mentions = null zapRecipients = mutableStateListOf() - pollOptions = mutableStateListOf("", "") + pollOptions = mutableStateMapOf(Pair(0, ""), Pair(1, "")) valueMaximum = null valueMinimum = null consensusThreshold = null closedAt = null } - - private fun getPollOptionsList(): List> { - val optionsList: MutableList> = mutableListOf() - pollOptions.forEachIndexed { i, s -> - optionsList.add(mapOf(Pair(i, s))) - } - return optionsList - } +} + +@Keep // Do not obfuscate! Variable names are needed for parsers +data class PollOptions(var poll_options: List) +fun parseJsonPollOption(json: String): PollOptions { + val typeToken = object : TypeToken() {}.type + return Gson().fromJson(json, typeToken) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt index 441c71142..0e275ecc7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt @@ -29,7 +29,7 @@ fun PollOption(pollViewModel: NewPollViewModel, optionIndex: Int) { OutlinedTextField( modifier = Modifier .weight(1F), - value = pollViewModel.pollOptions[optionIndex], + value = pollViewModel.pollOptions[optionIndex] ?: "", onValueChange = { pollViewModel.pollOptions[optionIndex] = it }, label = { Text( @@ -43,14 +43,14 @@ fun PollOption(pollViewModel: NewPollViewModel, optionIndex: Int) { color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) }, - colors = if (pollViewModel.pollOptions[optionIndex].isNotEmpty()) colorValid else colorInValid + colors = if (pollViewModel.pollOptions[optionIndex]?.isNotEmpty() == true) colorValid else colorInValid ) if (optionIndex > 1) { Button( modifier = Modifier .padding(start = 6.dp, top = 2.dp) .imePadding(), - onClick = { pollViewModel.pollOptions.removeAt(optionIndex) }, + 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) From 3cdfdfa8e8640e136a3c4962e120261e1373aecd Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Mon, 20 Mar 2023 09:52:23 +0900 Subject: [PATCH 20/36] rewrite pollOptions json parser --- .../amethyst/ui/actions/NewPollViewModel.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 index 3119d65c4..2f256b8b3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollViewModel.kt @@ -1,14 +1,12 @@ package com.vitorpamplona.amethyst.ui.actions -import androidx.annotation.Keep import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.text.input.TextFieldValue -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken import com.vitorpamplona.amethyst.model.* import com.vitorpamplona.amethyst.service.nip19.Nip19 +import org.json.JSONObject class NewPollViewModel : NewPostViewModel() { @@ -138,9 +136,11 @@ class NewPollViewModel : NewPostViewModel() { } } -@Keep // Do not obfuscate! Variable names are needed for parsers -data class PollOptions(var poll_options: List) -fun parseJsonPollOption(json: String): PollOptions { - val typeToken = object : TypeToken() {}.type - return Gson().fromJson(json, typeToken) +fun jsonToPollOptions(jsonString: String): Map { + val jsonMap = mutableMapOf() + val jsonObject = JSONObject(jsonString) + jsonObject.keys().forEach { + jsonMap[it.toString().toInt()] = jsonObject.getString(it) + } + return jsonMap } From bd37e4a9dfe022918670a2d144181279147d9b43 Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Mon, 20 Mar 2023 10:20:39 +0900 Subject: [PATCH 21/36] fix add new poll option button onclick, prefix all poll composables with 'New', move all poll composables to actions folder, move jsonToPollOptions parser to PollNoteEvent --- .../amethyst/service/model/PollNoteEvent.kt | 10 ++++++++-- .../PollClosing.kt => actions/NewPollClosing.kt} | 6 +++--- .../NewPollConsensusThreshold.kt} | 6 +++--- .../PollOption.kt => actions/NewPollOption.kt} | 9 ++++----- .../NewPollPrimaryDescription.kt} | 2 +- .../NewPollRecipientsField.kt} | 2 +- .../amethyst/ui/actions/NewPollView.kt | 14 +++++++------- .../amethyst/ui/actions/NewPollViewModel.kt | 10 ---------- .../NewPollVoteValueRange.kt} | 6 +++--- 9 files changed, 30 insertions(+), 35 deletions(-) rename app/src/main/java/com/vitorpamplona/amethyst/ui/{components/PollClosing.kt => actions/NewPollClosing.kt} (95%) rename app/src/main/java/com/vitorpamplona/amethyst/ui/{components/PollConsensusThreshold.kt => actions/NewPollConsensusThreshold.kt} (94%) rename app/src/main/java/com/vitorpamplona/amethyst/ui/{components/PollOption.kt => actions/NewPollOption.kt} (91%) rename app/src/main/java/com/vitorpamplona/amethyst/ui/{components/PollPrimaryDescription.kt => actions/NewPollPrimaryDescription.kt} (97%) rename app/src/main/java/com/vitorpamplona/amethyst/ui/{components/PollRecipientsField.kt => actions/NewPollRecipientsField.kt} (94%) rename app/src/main/java/com/vitorpamplona/amethyst/ui/{components/PollVoteValueRange.kt => actions/NewPollVoteValueRange.kt} (97%) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt index 91215db12..db0608076 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt @@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.service.model import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey import nostr.postr.Utils +import org.json.JSONObject import java.util.Date const val POLL_OPTIONS = "poll_options" @@ -73,8 +74,13 @@ class PollNoteEvent( return PollNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey()) } - fun parseJsonPollOptions(s: String): Map { - return gson.fromJson>(s, MutableMap::class.java) + fun jsonToPollOptions(jsonString: String): Map { + val jsonMap = mutableMapOf() + val jsonObject = JSONObject(jsonString) + jsonObject.keys().forEach { + jsonMap[it.toString().toInt()] = jsonObject.getString(it) + } + return jsonMap } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollClosing.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollClosing.kt similarity index 95% rename from app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollClosing.kt rename to app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollClosing.kt index c35b5faa7..ec4b7d631 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollClosing.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollClosing.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.actions.NewPollViewModel @Composable -fun PollClosing(pollViewModel: NewPollViewModel) { +fun NewPollClosing(pollViewModel: NewPollViewModel) { var text by rememberSaveable { mutableStateOf("") } pollViewModel.isValidClosedAt.value = true @@ -74,6 +74,6 @@ fun PollClosing(pollViewModel: NewPollViewModel) { @Preview @Composable -fun PollClosingPreview() { - PollClosing(NewPollViewModel()) +fun NewPollClosingPreview() { + NewPollClosing(NewPollViewModel()) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollConsensusThreshold.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollConsensusThreshold.kt similarity index 94% rename from app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollConsensusThreshold.kt rename to app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollConsensusThreshold.kt index bb8e3d42a..21e346f43 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollConsensusThreshold.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollConsensusThreshold.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.actions.NewPollViewModel @Composable -fun PollConsensusThreshold(pollViewModel: NewPollViewModel) { +fun NewPollConsensusThreshold(pollViewModel: NewPollViewModel) { var text by rememberSaveable { mutableStateOf("") } pollViewModel.isValidConsensusThreshold.value = true @@ -74,6 +74,6 @@ fun PollConsensusThreshold(pollViewModel: NewPollViewModel) { @Preview @Composable -fun PollConsensusThresholdPreview() { - PollConsensusThreshold(NewPollViewModel()) +fun NewPollConsensusThresholdPreview() { + NewPollConsensusThreshold(NewPollViewModel()) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollOption.kt similarity index 91% rename from app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt rename to app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollOption.kt index 0e275ecc7..c96af2a23 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollOption.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollOption.kt @@ -1,4 +1,4 @@ -package com.vitorpamplona.amethyst.ui.components +package com.vitorpamplona.amethyst.ui.actions import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image @@ -12,10 +12,9 @@ import androidx.compose.ui.res.stringResource 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 @Composable -fun PollOption(pollViewModel: NewPollViewModel, optionIndex: Int) { +fun NewPollOption(pollViewModel: NewPollViewModel, optionIndex: Int) { val colorInValid = TextFieldDefaults.outlinedTextFieldColors( focusedBorderColor = MaterialTheme.colors.error, unfocusedBorderColor = Color.Red @@ -68,6 +67,6 @@ fun PollOption(pollViewModel: NewPollViewModel, optionIndex: Int) { @Preview @Composable -fun PollOptionPreview() { - PollOption(NewPollViewModel(), 0) +fun NewPollOptionPreview() { + NewPollOption(NewPollViewModel(), 0) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollPrimaryDescription.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollPrimaryDescription.kt similarity index 97% rename from app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollPrimaryDescription.kt rename to app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollPrimaryDescription.kt index e68896f40..1084cf41c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollPrimaryDescription.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollPrimaryDescription.kt @@ -22,7 +22,7 @@ import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation @OptIn(ExperimentalComposeUiApi::class) @Composable -fun PollPrimaryDescription(pollViewModel: NewPollViewModel) { +fun NewPollPrimaryDescription(pollViewModel: NewPollViewModel) { // 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/components/PollRecipientsField.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollRecipientsField.kt similarity index 94% rename from app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollRecipientsField.kt rename to app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollRecipientsField.kt index 4498d2ad6..c8aac5cfc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollRecipientsField.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollRecipientsField.kt @@ -12,7 +12,7 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.actions.NewPollViewModel @Composable -fun PollRecipientsField(pollViewModel: NewPollViewModel, account: Account) { +fun NewPollRecipientsField(pollViewModel: NewPollViewModel, 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 ddab2e4fa..275948e69 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 @@ -114,13 +114,13 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n } Text(stringResource(R.string.poll_heading_required)) - PollRecipientsField(pollViewModel, account) - PollPrimaryDescription(pollViewModel) + NewPollRecipientsField(pollViewModel, account) + NewPollPrimaryDescription(pollViewModel) pollViewModel.pollOptions.values.forEachIndexed { index, element -> - PollOption(pollViewModel, index) + NewPollOption(pollViewModel, index) } Button( - onClick = { pollViewModel.pollOptions.values.add("") }, + onClick = { pollViewModel.pollOptions[pollViewModel.pollOptions.size] = "" }, border = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f)), colors = ButtonDefaults.outlinedButtonColors( contentColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) @@ -133,9 +133,9 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n ) } Text(stringResource(R.string.poll_heading_optional)) - PollVoteValueRange(pollViewModel) - PollConsensusThreshold(pollViewModel) - PollClosing(pollViewModel) + NewPollVoteValueRange(pollViewModel) + NewPollConsensusThreshold(pollViewModel) + NewPollClosing(pollViewModel) } } 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 index 2f256b8b3..b4c7f5f4c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollViewModel.kt @@ -6,7 +6,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.text.input.TextFieldValue import com.vitorpamplona.amethyst.model.* import com.vitorpamplona.amethyst.service.nip19.Nip19 -import org.json.JSONObject class NewPollViewModel : NewPostViewModel() { @@ -135,12 +134,3 @@ class NewPollViewModel : NewPostViewModel() { closedAt = null } } - -fun jsonToPollOptions(jsonString: String): Map { - val jsonMap = mutableMapOf() - val jsonObject = JSONObject(jsonString) - jsonObject.keys().forEach { - jsonMap[it.toString().toInt()] = jsonObject.getString(it) - } - return jsonMap -} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollVoteValueRange.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollVoteValueRange.kt similarity index 97% rename from app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollVoteValueRange.kt rename to app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollVoteValueRange.kt index 598290bf7..3c4ec0908 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/PollVoteValueRange.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollVoteValueRange.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.actions.NewPollViewModel @Composable -fun PollVoteValueRange(pollViewModel: NewPollViewModel) { +fun NewPollVoteValueRange(pollViewModel: NewPollViewModel) { var textMax by rememberSaveable { mutableStateOf("") } var textMin by rememberSaveable { mutableStateOf("") } @@ -122,6 +122,6 @@ fun PollVoteValueRange(pollViewModel: NewPollViewModel) { @Preview @Composable -fun PollVoteValueRangePreview() { - PollVoteValueRange(NewPollViewModel()) +fun NewPollVoteValueRangePreview() { + NewPollVoteValueRange(NewPollViewModel()) } From 5d04f3ea99a9415ccefb895e564b4a2b9aff8411 Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Mon, 20 Mar 2023 15:36:12 +0900 Subject: [PATCH 22/36] enable send poll event, add PollNoteEvent.kind to DataSources, add consume(PollNoteEvent) to LocalCache, --- .../vitorpamplona/amethyst/model/Account.kt | 6 +- .../amethyst/model/LocalCache.kt | 82 ++++++++++++++----- .../amethyst/service/NostrDataSource.kt | 23 +----- .../service/NostrSingleEventDataSource.kt | 23 ++---- 4 files changed, 72 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 5276a14cd..ca7ae2b0e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -304,9 +304,9 @@ class Account( consensusThreshold = consensusThreshold, closedAt = closedAt ) - println("PollNoteEvent: %s".format(signedEvent.toJson())) - // Client.send(signedEvent) - // LocalCache.consume(signedEvent) + println("Sending new PollNoteEvent: %s".format(signedEvent.toJson())) + Client.send(signedEvent) + LocalCache.consume(signedEvent) } fun sendChannelMessage(message: String, toChannel: String, replyingTo: Note? = null, mentions: List?) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index f12e48acc..30f8479f9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -5,28 +5,7 @@ import androidx.lifecycle.LiveData import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.google.gson.reflect.TypeToken -import com.vitorpamplona.amethyst.service.model.ATag -import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent -import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent -import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent -import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent -import com.vitorpamplona.amethyst.service.model.ChannelHideMessageEvent -import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent -import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent -import com.vitorpamplona.amethyst.service.model.ChannelMuteUserEvent -import com.vitorpamplona.amethyst.service.model.ContactListEvent -import com.vitorpamplona.amethyst.service.model.DeletionEvent -import com.vitorpamplona.amethyst.service.model.Event -import com.vitorpamplona.amethyst.service.model.LnZapEvent -import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent -import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent -import com.vitorpamplona.amethyst.service.model.MetadataEvent -import com.vitorpamplona.amethyst.service.model.PrivateDmEvent -import com.vitorpamplona.amethyst.service.model.ReactionEvent -import com.vitorpamplona.amethyst.service.model.RecommendRelayEvent -import com.vitorpamplona.amethyst.service.model.ReportEvent -import com.vitorpamplona.amethyst.service.model.RepostEvent -import com.vitorpamplona.amethyst.service.model.TextNoteEvent +import com.vitorpamplona.amethyst.service.model.* import com.vitorpamplona.amethyst.service.relays.Relay import fr.acinq.secp256k1.Hex import kotlinx.coroutines.CoroutineScope @@ -253,6 +232,51 @@ object LocalCache { } } + fun consume(event: PollNoteEvent, relay: Relay? = null) { + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) + + if (relay != null) { + author.addRelayBeingUsed(relay, event.createdAt) + note.addRelay(relay) + } + + // Already processed this event. + if (note.event != null) return + + if (antiSpam.isSpam(event)) { + relay?.let { + it.spamCounter++ + } + return + } + + val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) } + val replyTo = tagsWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) } + + note.loadEvent(event, author, mentions, replyTo) + + // Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content()?.take(100)} ${formattedDateTime(event.createdAt)}") + + // Prepares user's profile view. + author.addNote(note) + + // Adds notifications to users. + mentions.forEach { + it.addTaggedPost(note) + } + replyTo.forEach { + it.author?.addTaggedPost(note) + } + + // Counts the replies + replyTo.forEach { + it.addReply(note) + } + + refreshObservers() + } + fun consume(event: BadgeDefinitionEvent) { val note = getOrCreateAddressableNote(event.address()) val author = getOrCreateUser(event.pubKey) @@ -360,6 +384,20 @@ object LocalCache { } } + private fun tagsWithoutCitations(event: PollNoteEvent): List { + val repliesTo = event.replyTos() + val tagAddresses = event.taggedAddresses().map { it.toTag() } + if (repliesTo.isEmpty() && tagAddresses.isEmpty()) return emptyList() + + val citations = findCitations(event) + + return if (citations.isEmpty()) { + repliesTo + tagAddresses + } else { + repliesTo.filter { it !in citations } + } + } + fun consume(event: RecommendRelayEvent) { // Log.d("RR", event.toJson()) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt index 9df9931af..d92f84908 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt @@ -2,27 +2,7 @@ package com.vitorpamplona.amethyst.service import android.util.Log import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent -import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent -import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent -import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent -import com.vitorpamplona.amethyst.service.model.ChannelHideMessageEvent -import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent -import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent -import com.vitorpamplona.amethyst.service.model.ChannelMuteUserEvent -import com.vitorpamplona.amethyst.service.model.ContactListEvent -import com.vitorpamplona.amethyst.service.model.DeletionEvent -import com.vitorpamplona.amethyst.service.model.Event -import com.vitorpamplona.amethyst.service.model.LnZapEvent -import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent -import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent -import com.vitorpamplona.amethyst.service.model.MetadataEvent -import com.vitorpamplona.amethyst.service.model.PrivateDmEvent -import com.vitorpamplona.amethyst.service.model.ReactionEvent -import com.vitorpamplona.amethyst.service.model.RecommendRelayEvent -import com.vitorpamplona.amethyst.service.model.ReportEvent -import com.vitorpamplona.amethyst.service.model.RepostEvent -import com.vitorpamplona.amethyst.service.model.TextNoteEvent +import com.vitorpamplona.amethyst.service.model.* import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.Subscription @@ -91,6 +71,7 @@ abstract class NostrDataSource(val debugName: String) { LocalCache.consume(event) } is TextNoteEvent -> LocalCache.consume(event, relay) + is PollNoteEvent -> LocalCache.consume(event, relay) else -> { Log.w("Event Not Supported", event.toJson()) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt index 440744fe8..19b815453 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt @@ -2,19 +2,7 @@ package com.vitorpamplona.amethyst.service import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent -import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent -import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent -import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent -import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent -import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent -import com.vitorpamplona.amethyst.service.model.LnZapEvent -import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent -import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent -import com.vitorpamplona.amethyst.service.model.ReactionEvent -import com.vitorpamplona.amethyst.service.model.ReportEvent -import com.vitorpamplona.amethyst.service.model.RepostEvent -import com.vitorpamplona.amethyst.service.model.TextNoteEvent +import com.vitorpamplona.amethyst.service.model.* import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.amethyst.service.relays.TypedFilter @@ -45,7 +33,8 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") { TextNoteEvent.kind, LongTextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, ReportEvent.kind, LnZapEvent.kind, LnZapRequestEvent.kind, - BadgeAwardEvent.kind, BadgeDefinitionEvent.kind, BadgeProfilesEvent.kind + BadgeAwardEvent.kind, BadgeDefinitionEvent.kind, BadgeProfilesEvent.kind, + PollNoteEvent.kind ), tags = mapOf("a" to listOf(aTag.toTag())), since = it.lastReactionsDownloadTime @@ -104,7 +93,8 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") { RepostEvent.kind, ReportEvent.kind, LnZapEvent.kind, - LnZapRequestEvent.kind + LnZapRequestEvent.kind, + PollNoteEvent.kind ), tags = mapOf("e" to listOf(it.idHex)), since = it.lastReactionsDownloadTime @@ -137,7 +127,8 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") { filter = JsonFilter( kinds = listOf( TextNoteEvent.kind, LongTextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, LnZapEvent.kind, LnZapRequestEvent.kind, - ChannelMessageEvent.kind, ChannelCreateEvent.kind, ChannelMetadataEvent.kind, BadgeDefinitionEvent.kind, BadgeAwardEvent.kind, BadgeProfilesEvent.kind + ChannelMessageEvent.kind, ChannelCreateEvent.kind, ChannelMetadataEvent.kind, BadgeDefinitionEvent.kind, BadgeAwardEvent.kind, BadgeProfilesEvent.kind, + PollNoteEvent.kind ), ids = interestedEvents.toList() ) From d1f61c0bba7de512e599f62a73eba2968af4b60f Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Mon, 20 Mar 2023 17:38:14 +0900 Subject: [PATCH 23/36] add PollNoteEvent type to all relevant filters, dataSources, etc. --- .../amethyst/model/LocalCache.kt | 1 + .../service/NostrAccountDataSource.kt | 12 +----- .../amethyst/service/NostrGlobalDataSource.kt | 3 +- .../amethyst/service/NostrHomeDataSource.kt | 3 +- .../NostrSearchEventOrUserDataSource.kt | 9 +---- .../service/NostrUserProfileDataSource.kt | 10 +---- .../amethyst/ui/dal/GlobalFeedFilter.kt | 3 +- .../ui/dal/HomeConversationsFeedFilter.kt | 3 +- .../ui/dal/HomeNewThreadFeedFilter.kt | 5 ++- .../amethyst/ui/dal/NotificationFeedFilter.kt | 5 +++ .../amethyst/ui/note/NoteCompose.kt | 37 +++++++++++++------ 11 files changed, 49 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 30f8479f9..d6cf919bd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -803,6 +803,7 @@ object LocalCache { fun findNotesStartingWith(text: String): List { return notes.values.filter { (it.event is TextNoteEvent && it.event?.content()?.contains(text, true) ?: false) || + (it.event is PollNoteEvent && it.event?.content()?.contains(text, true) ?: false) || (it.event is ChannelMessageEvent && it.event?.content()?.contains(text, true) ?: false) || it.idHex.startsWith(text, true) || it.idNote().startsWith(text, true) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt index fac4279b9..5bd2a4e09 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt @@ -1,16 +1,7 @@ package com.vitorpamplona.amethyst.service import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent -import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent -import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent -import com.vitorpamplona.amethyst.service.model.ContactListEvent -import com.vitorpamplona.amethyst.service.model.LnZapEvent -import com.vitorpamplona.amethyst.service.model.MetadataEvent -import com.vitorpamplona.amethyst.service.model.ReactionEvent -import com.vitorpamplona.amethyst.service.model.ReportEvent -import com.vitorpamplona.amethyst.service.model.RepostEvent -import com.vitorpamplona.amethyst.service.model.TextNoteEvent +import com.vitorpamplona.amethyst.service.model.* import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.amethyst.service.relays.TypedFilter @@ -66,6 +57,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") { filter = JsonFilter( kinds = listOf( TextNoteEvent.kind, + PollNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, ReportEvent.kind, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt index 4a8830dea..dbcb987e6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrGlobalDataSource.kt @@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.service import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent +import com.vitorpamplona.amethyst.service.model.PollNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.JsonFilter @@ -11,7 +12,7 @@ object NostrGlobalDataSource : NostrDataSource("GlobalFeed") { fun createGlobalFilter() = TypedFilter( types = setOf(FeedType.GLOBAL), filter = JsonFilter( - kinds = listOf(TextNoteEvent.kind, ChannelMessageEvent.kind, LongTextNoteEvent.kind), + kinds = listOf(TextNoteEvent.kind, PollNoteEvent.kind, ChannelMessageEvent.kind, LongTextNoteEvent.kind), limit = 200 ) ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt index 6a07042f9..b49083b96 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt @@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.service import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.UserState import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent +import com.vitorpamplona.amethyst.service.model.PollNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.JsonFilter @@ -48,7 +49,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") { return TypedFilter( types = setOf(FeedType.FOLLOWS), filter = JsonFilter( - kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind), + kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, PollNoteEvent.kind), authors = followSet, limit = 400 ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt index 63d601b16..b5ee75889 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt @@ -1,12 +1,7 @@ package com.vitorpamplona.amethyst.service import com.vitorpamplona.amethyst.model.decodePublicKey -import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent -import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent -import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent -import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent -import com.vitorpamplona.amethyst.service.model.MetadataEvent -import com.vitorpamplona.amethyst.service.model.TextNoteEvent +import com.vitorpamplona.amethyst.service.model.* import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.amethyst.service.relays.TypedFilter @@ -65,7 +60,7 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SingleEventFeed") { TypedFilter( types = FeedType.values().toSet(), filter = JsonFilter( - kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, ChannelMetadataEvent.kind, ChannelCreateEvent.kind, ChannelMessageEvent.kind), + kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, PollNoteEvent.kind, ChannelMetadataEvent.kind, ChannelCreateEvent.kind, ChannelMessageEvent.kind), search = mySearchString, limit = 20 ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt index 39c85a97b..6b18cb950 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt @@ -2,13 +2,7 @@ package com.vitorpamplona.amethyst.service import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent -import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent -import com.vitorpamplona.amethyst.service.model.ContactListEvent -import com.vitorpamplona.amethyst.service.model.LnZapEvent -import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent -import com.vitorpamplona.amethyst.service.model.MetadataEvent -import com.vitorpamplona.amethyst.service.model.TextNoteEvent +import com.vitorpamplona.amethyst.service.model.* import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.amethyst.service.relays.TypedFilter @@ -41,7 +35,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") { TypedFilter( types = FeedType.values().toSet(), filter = JsonFilter( - kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind), + kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, PollNoteEvent.kind), authors = listOf(it.pubkeyHex), limit = 200 ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt index 5eda2e560..2a477a503 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt @@ -5,6 +5,7 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent +import com.vitorpamplona.amethyst.service.model.PollNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent object GlobalFeedFilter : FeedFilter() { @@ -12,7 +13,7 @@ object GlobalFeedFilter : FeedFilter() { override fun feed() = LocalCache.notes.values .filter { - (it.event is TextNoteEvent || it.event is LongTextNoteEvent || it.event is ChannelMessageEvent) && + (it.event is TextNoteEvent || it.event is LongTextNoteEvent || it.event is PollNoteEvent || it.event is ChannelMessageEvent) && it.replyTo.isNullOrEmpty() } .filter { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt index 8b2101ce1..832fe7299 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt @@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.model.PollNoteEvent import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent @@ -14,7 +15,7 @@ object HomeConversationsFeedFilter : FeedFilter() { return LocalCache.notes.values .filter { - (it.event is TextNoteEvent || it.event is RepostEvent) && + (it.event is TextNoteEvent || it.event is PollNoteEvent || it.event is RepostEvent) && it.author in user.follows && // && account.isAcceptable(it) // This filter follows only. No need to check if acceptable it.author?.let { !HomeNewThreadFeedFilter.account.isHidden(it) } ?: true && diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt index 45b220dd9..72030d8e8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt @@ -4,6 +4,7 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent +import com.vitorpamplona.amethyst.service.model.PollNoteEvent import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent @@ -15,7 +16,7 @@ object HomeNewThreadFeedFilter : FeedFilter() { val notes = LocalCache.notes.values .filter { it -> - (it.event is TextNoteEvent || it.event is RepostEvent || it.event is LongTextNoteEvent) && + (it.event is TextNoteEvent || it.event is RepostEvent || it.event is LongTextNoteEvent || it.event is PollNoteEvent) && it.author in user.follows && // && account.isAcceptable(it) // This filter follows only. No need to check if acceptable it.author?.let { !account.isHidden(it) } ?: true && @@ -24,7 +25,7 @@ object HomeNewThreadFeedFilter : FeedFilter() { val longFormNotes = LocalCache.addressables.values .filter { it -> - (it.event is TextNoteEvent || it.event is RepostEvent || it.event is LongTextNoteEvent) && + (it.event is TextNoteEvent || it.event is RepostEvent || it.event is LongTextNoteEvent || it.event is PollNoteEvent) && it.author in user.follows && // && account.isAcceptable(it) // This filter follows only. No need to check if acceptable it.author?.let { !account.isHidden(it) } ?: true && diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt index 707d93a50..4990d292d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt @@ -27,6 +27,11 @@ object NotificationFeedFilter : FeedFilter() { it.replyTo?.any { it.author == account.userProfile() } == true || account.userProfile() in it.directlyCiteUsers() } + .filter { it -> + it.event !is PollNoteEvent || + it.replyTo?.any { it.author == account.userProfile() } == true || + account.userProfile() in it.directlyCiteUsers() + } .filter { it.event !is ReactionEvent || it.replyTo?.lastOrNull()?.author == account.userProfile() || 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 44a958db5..c006f2cac 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 @@ -42,17 +42,7 @@ import com.vitorpamplona.amethyst.RoboHashCache import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent -import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent -import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent -import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent -import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent -import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent -import com.vitorpamplona.amethyst.service.model.PrivateDmEvent -import com.vitorpamplona.amethyst.service.model.ReactionEvent -import com.vitorpamplona.amethyst.service.model.ReportEvent -import com.vitorpamplona.amethyst.service.model.RepostEvent -import com.vitorpamplona.amethyst.service.model.TextNoteEvent +import com.vitorpamplona.amethyst.service.model.* import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status import com.vitorpamplona.amethyst.ui.components.ResizeImage @@ -324,6 +314,31 @@ fun NoteCompose( } } + if (noteEvent is PollNoteEvent && (note.replyTo != null || note.mentions != null)) { + val replyingDirectlyTo = note.replyTo?.lastOrNull() + if (replyingDirectlyTo != null && unPackReply) { + NoteCompose( + baseNote = replyingDirectlyTo, + isQuotedNote = true, + modifier = Modifier + .padding(0.dp) + .fillMaxWidth() + .clip(shape = RoundedCornerShape(15.dp)) + .border( + 1.dp, + MaterialTheme.colors.onSurface.copy(alpha = 0.12f), + RoundedCornerShape(15.dp) + ), + unPackReply = false, + makeItShort = true, + parentBackgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.05f) + .compositeOver(backgroundColor), + accountViewModel = accountViewModel, + navController = navController + ) + } + } + if (noteEvent is ReactionEvent || noteEvent is RepostEvent) { note.replyTo?.lastOrNull()?.let { NoteCompose( From 771cdd6ebeaf7b2b54952f2d1b748a4841824308 Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Tue, 21 Mar 2023 20:05:53 +0900 Subject: [PATCH 24/36] add PollNote to ui.note, simplify code for displaying PollNoteEvents with replyTos or mentions, fix pollOptions (and other poll fields) get from tags functions --- .../amethyst/service/model/PollNoteEvent.kt | 18 +++++++--- .../amethyst/ui/note/NoteCompose.kt | 31 +++------------- .../amethyst/ui/note/PollNote.kt | 35 +++++++++++++++++++ 3 files changed, 53 insertions(+), 31 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt index db0608076..32e7e59c1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt @@ -31,11 +31,19 @@ class PollNoteEvent( fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } - fun pollOptions() = tags.filter { it.firstOrNull() == POLL_OPTIONS }.mapNotNull { it.getOrNull(1) } - fun valueMaximum() = tags.filter { it.firstOrNull() == VALUE_MAXIMUM }.mapNotNull { it.getOrNull(1) } - fun valueMinimum() = tags.filter { it.firstOrNull() == VALUE_MINIMUM }.mapNotNull { it.getOrNull(1) } - fun consensusThreshold() = tags.filter { it.firstOrNull() == CONSENSUS_THRESHOLD }.mapNotNull { it.getOrNull(1) } - fun closedAt() = tags.filter { it.firstOrNull() == CLOSED_AT }.mapNotNull { it.getOrNull(1) } + fun pollOptions() = jsonToPollOptions(tags.filter { it.firstOrNull() == POLL_OPTIONS }[0][1]) + fun valueMaximum() = tags.filter { it.firstOrNull() == VALUE_MAXIMUM }.mapNotNull { + it.getOrNull(1)?.get(1) + } + fun valueMinimum() = tags.filter { it.firstOrNull() == VALUE_MINIMUM }.mapNotNull { + it.getOrNull(1)?.get(1) + } + fun consensusThreshold() = tags.filter { it.firstOrNull() == CONSENSUS_THRESHOLD }.mapNotNull { + it.getOrNull(1)?.get(1) + } + fun closedAt() = tags.filter { it.firstOrNull() == CLOSED_AT }.mapNotNull { + it.getOrNull(1)?.get(1) + } companion object { const val kind = 6969 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 c006f2cac..6806c7bd0 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 @@ -282,7 +282,7 @@ fun NoteCompose( Spacer(modifier = Modifier.height(3.dp)) - if (noteEvent is TextNoteEvent && (note.replyTo != null || note.mentions != null)) { + if ((noteEvent is TextNoteEvent || noteEvent is PollNoteEvent) && (note.replyTo != null || note.mentions != null)) { val replyingDirectlyTo = note.replyTo?.lastOrNull() if (replyingDirectlyTo != null && unPackReply) { NoteCompose( @@ -314,31 +314,6 @@ fun NoteCompose( } } - if (noteEvent is PollNoteEvent && (note.replyTo != null || note.mentions != null)) { - val replyingDirectlyTo = note.replyTo?.lastOrNull() - if (replyingDirectlyTo != null && unPackReply) { - NoteCompose( - baseNote = replyingDirectlyTo, - isQuotedNote = true, - modifier = Modifier - .padding(0.dp) - .fillMaxWidth() - .clip(shape = RoundedCornerShape(15.dp)) - .border( - 1.dp, - MaterialTheme.colors.onSurface.copy(alpha = 0.12f), - RoundedCornerShape(15.dp) - ), - unPackReply = false, - makeItShort = true, - parentBackgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.05f) - .compositeOver(backgroundColor), - accountViewModel = accountViewModel, - navController = navController - ) - } - } - if (noteEvent is ReactionEvent || noteEvent is RepostEvent) { note.replyTo?.lastOrNull()?.let { NoteCompose( @@ -469,6 +444,10 @@ fun NoteCompose( accountViewModel, navController ) + + if (noteEvent is PollNoteEvent) { + PollNote(noteEvent, canPreview, makeItShort, accountViewModel, navController) + } } if (!makeItShort) { 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 new file mode 100644 index 000000000..5df027a2b --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt @@ -0,0 +1,35 @@ +package com.vitorpamplona.amethyst.ui.note + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.MaterialTheme +import androidx.compose.material.SnackbarDefaults.backgroundColor +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import com.vitorpamplona.amethyst.service.model.PollNoteEvent +import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel + +@Composable +fun PollNote( + pollEvent: PollNoteEvent, + canPreview: Boolean, + makeItShort: Boolean, + accountViewModel: AccountViewModel, + navController: NavController +) { + pollEvent.pollOptions().values.forEachIndexed { index, string -> + TranslateableRichTextViewer( + string, + canPreview = canPreview && !makeItShort, + Modifier.fillMaxWidth().border(BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f))), + pollEvent.tags(), + backgroundColor, + accountViewModel, + navController + ) + } +} From ef31f56eab0ebc1da7547331bca889104e976f8b Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Tue, 21 Mar 2023 20:52:30 +0900 Subject: [PATCH 25/36] add PollNote type to ThreadFeedView, simplify PollNote() arguments, use parent's backgroundColor, breakout PollNote modifier --- .../amethyst/ui/note/NoteCompose.kt | 8 ++++++- .../amethyst/ui/note/PollNote.kt | 13 +++++++---- .../amethyst/ui/screen/ThreadFeedView.kt | 22 +++++++++++-------- 3 files changed, 29 insertions(+), 14 deletions(-) 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 6806c7bd0..fa40d1f7d 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 @@ -446,7 +446,13 @@ fun NoteCompose( ) if (noteEvent is PollNoteEvent) { - PollNote(noteEvent, canPreview, makeItShort, accountViewModel, navController) + PollNote( + noteEvent, + canPreview = canPreview && !makeItShort, + backgroundColor, + accountViewModel, + navController + ) } } 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 5df027a2b..a74018718 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 @@ -3,10 +3,11 @@ package com.vitorpamplona.amethyst.ui.note import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.border import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material.MaterialTheme -import androidx.compose.material.SnackbarDefaults.backgroundColor import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.vitorpamplona.amethyst.service.model.PollNoteEvent @@ -17,15 +18,19 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel fun PollNote( pollEvent: PollNoteEvent, canPreview: Boolean, - makeItShort: Boolean, + backgroundColor: Color, accountViewModel: AccountViewModel, navController: NavController ) { + val modifier = Modifier.fillMaxWidth() + .border(BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f))) + .padding(4.dp) + pollEvent.pollOptions().values.forEachIndexed { index, string -> TranslateableRichTextViewer( string, - canPreview = canPreview && !makeItShort, - Modifier.fillMaxWidth().border(BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f))), + canPreview, + modifier, pollEvent.tags(), backgroundColor, accountViewModel, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index fc40ed0ad..a9adaf72e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -17,6 +17,7 @@ import androidx.compose.material.Divider import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme +import androidx.compose.material.SnackbarDefaults.backgroundColor import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert @@ -48,17 +49,10 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent +import com.vitorpamplona.amethyst.service.model.PollNoteEvent import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer -import com.vitorpamplona.amethyst.ui.note.BadgeDisplay -import com.vitorpamplona.amethyst.ui.note.BlankNote -import com.vitorpamplona.amethyst.ui.note.HiddenNote -import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture -import com.vitorpamplona.amethyst.ui.note.NoteCompose -import com.vitorpamplona.amethyst.ui.note.NoteDropDownMenu -import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay -import com.vitorpamplona.amethyst.ui.note.ReactionsRow -import com.vitorpamplona.amethyst.ui.note.timeAgo +import com.vitorpamplona.amethyst.ui.note.* import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.coroutines.delay @@ -332,6 +326,16 @@ fun NoteMaster( accountViewModel, navController ) + + if (noteEvent is PollNoteEvent) { + PollNote( + noteEvent, + canPreview, + backgroundColor, + accountViewModel, + navController + ) + } } ReactionsRow(note, accountViewModel) From 611dcc01891609ea9d7d43963106f9952e79d90a Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Thu, 23 Mar 2023 16:18:16 +0900 Subject: [PATCH 26/36] implement new NIP69 poll_option tags format List>, fix PollNoteEvent field getter return types --- .../amethyst/service/model/PollNoteEvent.kt | 53 ++++++++----------- .../amethyst/ui/actions/NewPollViewModel.kt | 9 +++- .../amethyst/ui/note/PollNote.kt | 4 +- 3 files changed, 32 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt index 32e7e59c1..c53cf79ff 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt @@ -3,10 +3,9 @@ package com.vitorpamplona.amethyst.service.model import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey import nostr.postr.Utils -import org.json.JSONObject import java.util.Date -const val POLL_OPTIONS = "poll_options" +const val POLL_OPTION = "poll_option" const val VALUE_MAXIMUM = "value_maximum" const val VALUE_MINIMUM = "value_minimum" const val CONSENSUS_THRESHOLD = "consensus_threshold" @@ -31,19 +30,23 @@ class PollNoteEvent( fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } - fun pollOptions() = jsonToPollOptions(tags.filter { it.firstOrNull() == POLL_OPTIONS }[0][1]) - fun valueMaximum() = tags.filter { it.firstOrNull() == VALUE_MAXIMUM }.mapNotNull { - it.getOrNull(1)?.get(1) - } - fun valueMinimum() = tags.filter { it.firstOrNull() == VALUE_MINIMUM }.mapNotNull { - it.getOrNull(1)?.get(1) - } - fun consensusThreshold() = tags.filter { it.firstOrNull() == CONSENSUS_THRESHOLD }.mapNotNull { - it.getOrNull(1)?.get(1) - } - fun closedAt() = tags.filter { it.firstOrNull() == CLOSED_AT }.mapNotNull { - it.getOrNull(1)?.get(1) + fun pollOptions(): Map { + val map = mutableMapOf() + tags.filter { it.first() == POLL_OPTION } + .forEach { map[it[1].toInt()] = it[2] } + return map } + fun valueMaximum(): Int? = tags.filter { it.firstOrNull() == VALUE_MAXIMUM } + .getOrNull(1)?.getOrNull(1)?.toInt() + + fun valueMinimum(): Int? = tags.filter { it.firstOrNull() == VALUE_MINIMUM } + .getOrNull(1)?.getOrNull(1)?.toInt() + + fun consensusThreshold(): Int? = tags.filter { it.firstOrNull() == CONSENSUS_THRESHOLD } + .getOrNull(1)?.getOrNull(1)?.toInt() + + fun closedAt(): Int? = tags.filter { it.firstOrNull() == CLOSED_AT } + .getOrNull(1)?.getOrNull(1)?.toInt() companion object { const val kind = 6969 @@ -72,7 +75,9 @@ class PollNoteEvent( addresses?.forEach { tags.add(listOf("a", it.toTag())) } - tags.add(listOf(POLL_OPTIONS, gson.toJson(pollOptions))) + pollOptions.forEach { poll_op -> + tags.add(listOf(POLL_OPTION, poll_op.key.toString(), poll_op.value)) + } tags.add(listOf(VALUE_MAXIMUM, valueMaximum.toString())) tags.add(listOf(VALUE_MINIMUM, valueMinimum.toString())) tags.add(listOf(CONSENSUS_THRESHOLD, consensusThreshold.toString())) @@ -81,15 +86,6 @@ class PollNoteEvent( val sig = Utils.sign(id, privateKey) return PollNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey()) } - - fun jsonToPollOptions(jsonString: String): Map { - val jsonMap = mutableMapOf() - val jsonObject = JSONObject(jsonString) - jsonObject.keys().forEach { - jsonMap[it.toString().toInt()] = jsonObject.getString(it) - } - return jsonMap - } } } @@ -102,12 +98,9 @@ class PollNoteEvent( "tags": [ ["e", <32-bytes hex of the id of the poll event>, ], ["p", <32-bytes hex of the key>, ], - ["poll_options", "{ - \"0\": \"poll option 0 description string\", - \"1\": \"poll option 1 description string\", - \"n\": \"poll option description string\" - }" - ], + ["poll_option", "0", "poll option 0 description string"], + ["poll_option", "1", "poll option 1 description string"], + ["poll_option", "n", "poll option description string"], ["value_maximum", "maximum satoshi value for inclusion in tally"], ["value_minimum", "minimum satoshi value for inclusion in tally"], ["consensus_threshold", "required percentage to attain consensus <0..100>"], 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 index b4c7f5f4c..2bd10b6a2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollViewModel.kt @@ -3,6 +3,7 @@ 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 @@ -10,7 +11,7 @@ import com.vitorpamplona.amethyst.service.nip19.Nip19 class NewPollViewModel : NewPostViewModel() { var zapRecipients = mutableStateListOf() - var pollOptions = mutableStateMapOf(Pair(0, ""), Pair(1, "")) + var pollOptions = newStateMapPollOptions() var valueMaximum: Int? = null var valueMinimum: Int? = null var consensusThreshold: Int? = null @@ -127,10 +128,14 @@ class NewPollViewModel : NewPostViewModel() { mentions = null zapRecipients = mutableStateListOf() - pollOptions = mutableStateMapOf(Pair(0, ""), Pair(1, "")) + 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/note/PollNote.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt index a74018718..15f046029 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 @@ -26,9 +26,9 @@ fun PollNote( .border(BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f))) .padding(4.dp) - pollEvent.pollOptions().values.forEachIndexed { index, string -> + pollEvent.pollOptions().forEach { poll_op -> TranslateableRichTextViewer( - string, + poll_op.value, canPreview, modifier, pollEvent.tags(), From 171a7841b39e5c790977e6f2a5fe2d3eb937f72f Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Thu, 23 Mar 2023 19:11:07 +0900 Subject: [PATCH 27/36] add zap buttons to PollNote options, add poll_option tag and handlers to LnZapEvents and zap funs, --- .../vitorpamplona/amethyst/model/Account.kt | 11 +- .../amethyst/service/model/LnZapEvent.kt | 4 + .../service/model/LnZapEventInterface.kt | 2 + .../service/model/LnZapRequestEvent.kt | 19 +- .../amethyst/ui/note/NoteCompose.kt | 2 +- .../amethyst/ui/note/PollNote.kt | 212 ++++++++++++++++-- .../amethyst/ui/note/ReactionsRow.kt | 6 +- .../amethyst/ui/screen/ThreadFeedView.kt | 2 +- .../ui/screen/loggedIn/AccountViewModel.kt | 4 +- 9 files changed, 233 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index ca7ae2b0e..0daa97f58 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -124,11 +124,16 @@ class Account( } } - fun createZapRequestFor(note: Note): LnZapRequestEvent? { + fun createZapRequestFor(note: Note, pollOption: Int?): LnZapRequestEvent? { if (!isWriteable()) return null - note.event?.let { - return LnZapRequestEvent.create(it, userProfile().relays?.keys?.ifEmpty { null } ?: localRelays.map { it.url }.toSet(), loggedIn.privKey!!) + note.event?.let { event -> + return LnZapRequestEvent.create( + event, + userProfile().relays?.keys?.ifEmpty { null } ?: localRelays.map { it.url }.toSet(), + loggedIn.privKey!!, + pollOption + ) } return null diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt index b8845b8d5..a60118eaf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt @@ -19,6 +19,10 @@ class LnZapEvent( .filter { it.firstOrNull() == "e" } .mapNotNull { it.getOrNull(1) } + override fun zappedPollOption(): Int? = tags + .filter { it.firstOrNull() == "poll_option" } + .getOrNull(1)?.getOrNull(1)?.toInt() + override fun zappedAuthor() = tags .filter { it.firstOrNull() == "p" } .mapNotNull { it.getOrNull(1) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEventInterface.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEventInterface.kt index cc95d34b3..c6ca7efb3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEventInterface.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEventInterface.kt @@ -6,6 +6,8 @@ interface LnZapEventInterface : EventInterface { fun zappedPost(): List + fun zappedPollOption(): Int? + fun zappedAuthor(): List fun taggedAddresses(): List diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt index 75ce41ca2..4bf0f0d11 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt @@ -21,11 +21,19 @@ class LnZapRequestEvent( if (aTagValue != null) ATag.parse(aTagValue, relay) else null } + fun pollOption(): Int? = tags.filter { it.firstOrNull() == POLL_OPTION } + .getOrNull(1)?.getOrNull(1)?.toInt() companion object { const val kind = 9734 - fun create(originalNote: EventInterface, relays: Set, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent { + fun create( + originalNote: EventInterface, + relays: Set, + privateKey: ByteArray, + pollOption: Int?, + createdAt: Long = Date().time / 1000 + ): LnZapRequestEvent { val content = "" val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() var tags = listOf( @@ -36,6 +44,9 @@ class LnZapRequestEvent( if (originalNote is LongTextNoteEvent) { tags = tags + listOf(listOf("a", originalNote.address().toTag())) } + if (pollOption != null && pollOption >= 0) { + tags = tags + listOf(listOf(POLL_OPTION, pollOption.toString())) + } val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) @@ -85,7 +96,11 @@ class LnZapRequestEvent( "wss://nostr.bitcoiner.social", "ws://monad.jb55.com:8080", "wss://relay.snort.social" + ], + [ + "poll_option", "n" ] - ] + ], + "ots": // TODO } */ 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 fa40d1f7d..6497fa261 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 @@ -447,7 +447,7 @@ fun NoteCompose( if (noteEvent is PollNoteEvent) { PollNote( - noteEvent, + note, canPreview = canPreview && !makeItShort, backgroundColor, accountViewModel, 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 15f046029..6211017ec 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 @@ -1,40 +1,218 @@ package com.vitorpamplona.amethyst.ui.note +import android.widget.Toast import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.border -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material.MaterialTheme -import androidx.compose.runtime.Composable +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Bolt +import androidx.compose.material.icons.outlined.Bolt +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.runtime.* +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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.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.Popup import androidx.navigation.NavController +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.PollNoteEvent import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange +import kotlinx.coroutines.launch @Composable fun PollNote( - pollEvent: PollNoteEvent, + note: Note, canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, navController: NavController ) { - val modifier = Modifier.fillMaxWidth() - .border(BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f))) - .padding(4.dp) + val pollEvent = note.event as PollNoteEvent pollEvent.pollOptions().forEach { poll_op -> - TranslateableRichTextViewer( - poll_op.value, - canPreview, - modifier, - pollEvent.tags(), - backgroundColor, - accountViewModel, - navController - ) + Row(Modifier.fillMaxWidth()) { + val modifier = Modifier + .weight(1f) + .border(BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f))) + .padding(4.dp) + + TranslateableRichTextViewer( + poll_op.value, + canPreview, + modifier, + pollEvent.tags(), + backgroundColor, + accountViewModel, + navController + ) + + ZapVote(note, accountViewModel, poll_op.key) + } + } +} + +@Composable +@OptIn(ExperimentalFoundationApi::class) +fun ZapVote( + baseNote: Note, + accountViewModel: AccountViewModel, + pollOption: Int, + modifier: Modifier = Modifier +) { + val accountState by accountViewModel.accountLiveData.observeAsState() + val account = accountState?.account ?: return + + val zapsState by baseNote.live().zaps.observeAsState() + val zappedNote = zapsState?.note + + var wantsToZap by remember { mutableStateOf(false) } + + val context = LocalContext.current + val scope = rememberCoroutineScope() + + Row( + 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 (account.zapAmountChoices.size == 1) { + accountViewModel.zap( + baseNote, + account.zapAmountChoices.first() * 1000, + pollOption, + "", + context + ) { + scope.launch { + Toast + .makeText(context, it, Toast.LENGTH_SHORT) + .show() + } + } + } else if (account.zapAmountChoices.size > 1) { + wantsToZap = true + } + }, + onLongClick = {} + ) + ) { + if (wantsToZap) { + ZapVoteAmountChoicePopup( + baseNote, + accountViewModel, + pollOption, + onDismiss = { + wantsToZap = false + }, + onError = { + scope.launch { + Toast.makeText(context, it, Toast.LENGTH_SHORT).show() + } + } + ) + } + + if (zappedNote?.isZappedBy(account.userProfile()) == true) { + Icon( + imageVector = Icons.Default.Bolt, + contentDescription = stringResource(R.string.zaps), + modifier = Modifier.size(20.dp), + tint = BitcoinOrange + ) + } else { + Icon( + imageVector = Icons.Outlined.Bolt, + contentDescription = stringResource(id = R.string.zaps), + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + } + } + + Text( + showAmount(zappedNote?.zappedAmount()), + fontSize = 14.sp, + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), + modifier = modifier + ) +} + +@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class) +@Composable +fun ZapVoteAmountChoicePopup( + baseNote: Note, + accountViewModel: AccountViewModel, + pollOption: Int, + onDismiss: () -> Unit, + onError: (text: String) -> Unit +) { + val context = LocalContext.current + + val accountState by accountViewModel.accountLiveData.observeAsState() + val account = accountState?.account ?: return + + Popup( + alignment = Alignment.BottomCenter, + offset = IntOffset(0, -50), + onDismissRequest = { onDismiss() } + ) { + FlowRow(horizontalArrangement = Arrangement.Center) { + account.zapAmountChoices.forEach { amountInSats -> + Button( + modifier = Modifier.padding(horizontal = 3.dp), + onClick = { + accountViewModel.zap(baseNote, amountInSats * 1000, pollOption, "", context, onError) + 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 = { + accountViewModel.zap(baseNote, amountInSats * 1000, pollOption, "", context, onError) + onDismiss() + }, + onLongClick = {} + ) + ) + } + } + } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index b955120a7..2bd06d01c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -350,7 +350,7 @@ fun ZapReaction( .show() } } else if (account.zapAmountChoices.size == 1) { - accountViewModel.zap(baseNote, account.zapAmountChoices.first() * 1000, "", context) { + accountViewModel.zap(baseNote, account.zapAmountChoices.first() * 1000, null, "", context) { scope.launch { Toast .makeText(context, it, Toast.LENGTH_SHORT) @@ -502,7 +502,7 @@ fun ZapAmountChoicePopup(baseNote: Note, accountViewModel: AccountViewModel, onD Button( modifier = Modifier.padding(horizontal = 3.dp), onClick = { - accountViewModel.zap(baseNote, amountInSats * 1000, "", context, onError) + accountViewModel.zap(baseNote, amountInSats * 1000, null, "", context, onError) onDismiss() }, shape = RoundedCornerShape(20.dp), @@ -517,7 +517,7 @@ fun ZapAmountChoicePopup(baseNote: Note, accountViewModel: AccountViewModel, onD textAlign = TextAlign.Center, modifier = Modifier.combinedClickable( onClick = { - accountViewModel.zap(baseNote, amountInSats * 1000, "", context, onError) + accountViewModel.zap(baseNote, amountInSats * 1000, null, "", context, onError) onDismiss() }, onLongClick = { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index a9adaf72e..7322ada1b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -329,7 +329,7 @@ fun NoteMaster( if (noteEvent is PollNoteEvent) { PollNote( - noteEvent, + note, canPreview, backgroundColor, accountViewModel, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 2734c6ee1..50fc500cc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -48,7 +48,7 @@ class AccountViewModel(private val account: Account) : ViewModel() { account.delete(account.boostsTo(note)) } - fun zap(note: Note, amount: Long, message: String, context: Context, onError: (String) -> Unit) { + fun zap(note: Note, amount: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit) { val lud16 = note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim() if (lud16.isNullOrBlank()) { @@ -56,7 +56,7 @@ class AccountViewModel(private val account: Account) : ViewModel() { return } - val zapRequest = account.createZapRequestFor(note) + val zapRequest = account.createZapRequestFor(note, pollOption) LightningAddressResolver().lnAddressInvoice( lud16, From dbf0256b1c9828fb00c41c3c3e19f6de50ae64c3 Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Fri, 24 Mar 2023 18:10:02 +0900 Subject: [PATCH 28/36] add zap vote amount dialog, show vote option tallies, format zapped vote button icons --- .../com/vitorpamplona/amethyst/model/Note.kt | 15 ++ .../amethyst/service/model/LnZapEvent.kt | 7 + .../ui/actions/NewPollVoteValueRange.kt | 7 +- .../amethyst/ui/note/PollNote.kt | 194 ++++++++++++++---- app/src/main/res/values/strings.xml | 3 +- 5 files changed, 180 insertions(+), 46 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 8437d532b..32cc0563f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -237,6 +237,21 @@ open class Note(val idHex: String) { }.sumOf { it } } + fun zappedPollOptionAmount(option: Int): BigDecimal { + return zaps.mapNotNull { it.value?.event } + .filterIsInstance() + .mapNotNull { + val zappedOption = it.zappedPollOption() + if (zappedOption == option) { + it.amount + } else { null } + }.sumOf { it } + } + + fun isPollOptionZapped(option: Int): Boolean { + return zappedPollOptionAmount(option).toInt() > 0 + } + fun hasAnyReports(): Boolean { val dayAgo = Date().time / 1000 - 24 * 60 * 60 return reports.isNotEmpty() || diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt index a60118eaf..dd3a9bcf9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt @@ -19,9 +19,16 @@ class LnZapEvent( .filter { it.firstOrNull() == "e" } .mapNotNull { it.getOrNull(1) } +/* // TODO add poll_option tag to LnZapEvent override fun zappedPollOption(): Int? = tags .filter { it.firstOrNull() == "poll_option" } .getOrNull(1)?.getOrNull(1)?.toInt() +*/ + // TODO replace this hacky way to get poll option with above function + override fun zappedPollOption(): Int? = description() + ?.substringAfter("poll_option\",\"") + ?.substringBefore("\"") + ?.toInt() override fun zappedAuthor() = tags .filter { it.firstOrNull() == "p" } 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 3c4ec0908..63a075759 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 @@ -1,4 +1,4 @@ -package com.vitorpamplona.amethyst.ui.components +package com.vitorpamplona.amethyst.ui.actions import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row @@ -21,7 +21,6 @@ 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 @Composable fun NewPollVoteValueRange(pollViewModel: NewPollViewModel) { @@ -93,7 +92,7 @@ fun NewPollVoteValueRange(pollViewModel: NewPollViewModel) { }, placeholder = { Text( - text = stringResource(R.string.poll_zap_amount), + text = stringResource(R.string.sats), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) } @@ -112,7 +111,7 @@ fun NewPollVoteValueRange(pollViewModel: NewPollViewModel) { }, placeholder = { Text( - text = stringResource(R.string.poll_zap_amount), + text = stringResource(R.string.sats), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) } 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 6211017ec..c1611a41f 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 @@ -1,13 +1,11 @@ package com.vitorpamplona.amethyst.ui.note import android.widget.Toast -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.border -import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.* import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Bolt @@ -15,17 +13,18 @@ import androidx.compose.material.icons.outlined.Bolt import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.ui.Alignment +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier 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.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.Popup +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties import androidx.navigation.NavController import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note @@ -34,6 +33,7 @@ import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import kotlinx.coroutines.launch +import java.util.* @Composable fun PollNote( @@ -44,6 +44,7 @@ fun PollNote( navController: NavController ) { val pollEvent = note.event as PollNoteEvent + val consensusThreshold = pollEvent.consensusThreshold() pollEvent.pollOptions().forEach { poll_op -> Row(Modifier.fillMaxWidth()) { @@ -86,6 +87,15 @@ fun ZapVote( val context = LocalContext.current val scope = rememberCoroutineScope() + val pollEvent = baseNote.event as PollNoteEvent + val valueMaximum = pollEvent.valueMaximum() + val valueMinimum = pollEvent.valueMinimum() + + val isPollClosed: Boolean = pollEvent.closedAt()?.let { // allow 2 minute leeway for zap to propagate + baseNote.createdAt()?.plus(it * (86400 + 120))!! > Date().time / 1000 + } == true + val isVoteAmountAtomic = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum + Row( modifier = Modifier .then(Modifier.size(20.dp)) @@ -104,10 +114,20 @@ fun ZapVote( ) .show() } - } else if (account.zapAmountChoices.size == 1) { + } else if (isPollClosed) { + scope.launch { + Toast + .makeText( + context, + context.getString(R.string.poll_is_closed), + Toast.LENGTH_SHORT + ) + .show() + } + } else if (isVoteAmountAtomic) { accountViewModel.zap( baseNote, - account.zapAmountChoices.first() * 1000, + valueMaximum!!.toLong() * 1000, pollOption, "", context @@ -118,7 +138,7 @@ fun ZapVote( .show() } } - } else if (account.zapAmountChoices.size > 1) { + } else { wantsToZap = true } }, @@ -130,6 +150,8 @@ fun ZapVote( baseNote, accountViewModel, pollOption, + valueMinimum, + valueMaximum, onDismiss = { wantsToZap = false }, @@ -141,7 +163,7 @@ fun ZapVote( ) } - if (zappedNote?.isZappedBy(account.userProfile()) == true) { + if (zappedNote?.isPollOptionZapped(pollOption) == true) { Icon( imageVector = Icons.Default.Bolt, contentDescription = stringResource(R.string.zaps), @@ -159,7 +181,7 @@ fun ZapVote( } Text( - showAmount(zappedNote?.zappedAmount()), + showAmount(zappedNote?.zappedPollOptionAmount(pollOption)), fontSize = 14.sp, color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), modifier = modifier @@ -172,45 +194,135 @@ fun ZapVoteAmountChoicePopup( baseNote: Note, accountViewModel: AccountViewModel, pollOption: Int, + valueMinimum: Int?, + valueMaximum: Int?, onDismiss: () -> Unit, onError: (text: String) -> Unit ) { val context = LocalContext.current - val accountState by accountViewModel.accountLiveData.observeAsState() - val account = accountState?.account ?: return + var textAmount by rememberSaveable { mutableStateOf("") } - Popup( - alignment = Alignment.BottomCenter, - offset = IntOffset(0, -50), - onDismissRequest = { onDismiss() } + val placeHolderText = if (valueMinimum == null && valueMaximum == null) { + stringResource(R.string.sats) + } else if (valueMinimum == null) { + "1—$valueMaximum " + stringResource(R.string.sats) + } else if (valueMaximum == null) { + ">$valueMinimum " + stringResource(R.string.sats) + } else { + "$valueMinimum—$valueMaximum " + stringResource(R.string.sats) + } + + val amount = if (textAmount.isEmpty()) { null } else { + try { + textAmount.toLong() + } catch (e: Exception) { null } + } + + var isValidAmount = false + if (amount == null) { + isValidAmount = false + } else if (valueMinimum == null && valueMaximum == null) { + if (amount > 0) { + isValidAmount = true + } + } else if (valueMinimum == null) { + if (amount > 0 && amount <= valueMaximum!!) { + isValidAmount = true + } + } else if (valueMaximum == null) { + if (amount >= valueMinimum) { + isValidAmount = true + } + } else { + if ((valueMinimum <= amount) && (amount <= valueMaximum)) { + isValidAmount = true + } + } + + val colorInValid = TextFieldDefaults.outlinedTextFieldColors( + focusedBorderColor = MaterialTheme.colors.error, + unfocusedBorderColor = Color.Red + ) + val colorValid = TextFieldDefaults.outlinedTextFieldColors( + focusedBorderColor = MaterialTheme.colors.primary, + unfocusedBorderColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + + Dialog( + onDismissRequest = { onDismiss() }, + properties = DialogProperties( + dismissOnClickOutside = true, + usePlatformDefaultWidth = false + ) ) { - FlowRow(horizontalArrangement = Arrangement.Center) { - account.zapAmountChoices.forEach { amountInSats -> - Button( - modifier = Modifier.padding(horizontal = 3.dp), - onClick = { - accountViewModel.zap(baseNote, amountInSats * 1000, pollOption, "", context, onError) - onDismiss() + Surface { + Row( + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .background(MaterialTheme.colors.primary) + .padding(10.dp) + ) { + OutlinedTextField( + value = textAmount, + onValueChange = { textAmount = it }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.width(150.dp), + colors = if (isValidAmount) colorValid else colorInValid, + label = { + Text( + text = stringResource(R.string.poll_zap_amount), + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) }, - shape = RoundedCornerShape(20.dp), - colors = ButtonDefaults - .buttonColors( - backgroundColor = MaterialTheme.colors.primary + placeholder = { + Text( + text = placeHolderText, + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) - ) { - Text( - "⚡ ${showAmount(amountInSats.toBigDecimal().setScale(1))}", - color = Color.White, - textAlign = TextAlign.Center, - modifier = Modifier.combinedClickable( - onClick = { - accountViewModel.zap(baseNote, amountInSats * 1000, pollOption, "", context, onError) - onDismiss() - }, - onLongClick = {} + } + ) + + if (amount != null && isValidAmount) { + Button( + modifier = Modifier.padding(horizontal = 3.dp), + onClick = { + accountViewModel.zap( + baseNote, + amount * 1000, + pollOption, + "", + context, + onError + ) + onDismiss() + }, + shape = RoundedCornerShape(20.dp), + colors = ButtonDefaults + .buttonColors( + backgroundColor = MaterialTheme.colors.primary + ) + ) { + Text( + "⚡ ${showAmount(amount.toBigDecimal().setScale(1))}", + color = Color.White, + textAlign = TextAlign.Center, + modifier = Modifier.combinedClickable( + onClick = { + accountViewModel.zap( + baseNote, + amount * 1000, + pollOption, + "", + context, + onError + ) + onDismiss() + }, + onLongClick = {} + ) ) - ) + } } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2a5c1bdfd..0fa1498cc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -231,8 +231,9 @@ Zap maximum Consensus (0–100)% - sats Close after days + Poll is closed to new votes + Zap amount From 712c8ab2dd3a8607df058c42e8ec0ab8beccc64b Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Fri, 24 Mar 2023 20:26:58 +0900 Subject: [PATCH 29/36] fix isPollOptionZappedBy fun, option zap widgets layout improvements --- .../com/vitorpamplona/amethyst/model/Note.kt | 18 +++-- .../amethyst/ui/note/PollNote.kt | 72 ++++++++++--------- 2 files changed, 53 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 32cc0563f..283f47262 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -203,6 +203,20 @@ open class Note(val idHex: String) { return zaps.any { it.key.author == user } } + fun isPollOptionZappedBy(option: Int, user: User): Boolean { + if (zaps.any { it.key.author == user }) { + zaps.mapNotNull { it.value?.event } + .filterIsInstance() + .map { + val zappedOption = it.zappedPollOption() + if (zappedOption == option) { + return true + } + } + } + return false + } + fun isReactedBy(user: User): Boolean { return reactions.any { it.author == user } } @@ -248,10 +262,6 @@ open class Note(val idHex: String) { }.sumOf { it } } - fun isPollOptionZapped(option: Int): Boolean { - return zappedPollOptionAmount(option).toInt() > 0 - } - fun hasAnyReports(): Boolean { val dayAgo = Date().time / 1000 - 24 * 60 * 60 return reports.isNotEmpty() || 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 c1611a41f..31c9849aa 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 @@ -14,6 +14,7 @@ import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.* 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.graphics.Color import androidx.compose.ui.platform.LocalContext @@ -47,16 +48,17 @@ fun PollNote( val consensusThreshold = pollEvent.consensusThreshold() pollEvent.pollOptions().forEach { poll_op -> - Row(Modifier.fillMaxWidth()) { - val modifier = Modifier - .weight(1f) - .border(BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f))) - .padding(4.dp) - + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { TranslateableRichTextViewer( poll_op.value, canPreview, - modifier, + modifier = Modifier + .weight(1f) + .border(BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f))) + .padding(4.dp), pollEvent.tags(), backgroundColor, accountViewModel, @@ -97,6 +99,7 @@ fun ZapVote( val isVoteAmountAtomic = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum Row( + verticalAlignment = Alignment.CenterVertically, modifier = Modifier .then(Modifier.size(20.dp)) .combinedClickable( @@ -163,7 +166,7 @@ fun ZapVote( ) } - if (zappedNote?.isPollOptionZapped(pollOption) == true) { + if (zappedNote?.isPollOptionZappedBy(pollOption, account.userProfile()) == true) { Icon( imageVector = Icons.Default.Bolt, contentDescription = stringResource(R.string.zaps), @@ -188,7 +191,7 @@ fun ZapVote( ) } -@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class) +@OptIn(ExperimentalFoundationApi::class) @Composable fun ZapVoteAmountChoicePopup( baseNote: Note, @@ -259,8 +262,8 @@ fun ZapVoteAmountChoicePopup( Surface { Row( horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, modifier = Modifier - .background(MaterialTheme.colors.primary) .padding(10.dp) ) { OutlinedTextField( @@ -283,10 +286,11 @@ fun ZapVoteAmountChoicePopup( } ) - if (amount != null && isValidAmount) { - Button( - modifier = Modifier.padding(horizontal = 3.dp), - onClick = { + Button( + modifier = Modifier.padding(horizontal = 3.dp), + enabled = isValidAmount, + onClick = { + if (amount != null) { accountViewModel.zap( baseNote, amount * 1000, @@ -295,20 +299,22 @@ fun ZapVoteAmountChoicePopup( context, onError ) - onDismiss() - }, - shape = RoundedCornerShape(20.dp), - colors = ButtonDefaults - .buttonColors( - backgroundColor = MaterialTheme.colors.primary - ) - ) { - Text( - "⚡ ${showAmount(amount.toBigDecimal().setScale(1))}", - color = Color.White, - textAlign = TextAlign.Center, - modifier = Modifier.combinedClickable( - onClick = { + } + onDismiss() + }, + shape = RoundedCornerShape(20.dp), + colors = ButtonDefaults + .buttonColors( + backgroundColor = MaterialTheme.colors.primary + ) + ) { + Text( + "⚡ ${showAmount(amount?.toBigDecimal()?.setScale(1))}", + color = Color.White, + textAlign = TextAlign.Center, + modifier = Modifier.combinedClickable( + onClick = { + if (amount != null) { accountViewModel.zap( baseNote, amount * 1000, @@ -317,12 +323,12 @@ fun ZapVoteAmountChoicePopup( context, onError ) - onDismiss() - }, - onLongClick = {} - ) + } + onDismiss() + }, + onLongClick = {} ) - } + ) } } } From 9b9cc092dc7c014739d05dcf70f25c9f75d88a66 Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Sat, 25 Mar 2023 08:59:11 +0900 Subject: [PATCH 30/36] move PollNote logic to PollNoteViewModel, fix PollNoteEvent tag getter funs for the final fucking time (hopefully)!, only show option amounts if user has zapped poll note, make poll options wider --- .../amethyst/service/model/PollNoteEvent.kt | 21 ++-- .../amethyst/ui/note/PollNote.kt | 95 +++++-------------- .../amethyst/ui/note/PollNoteViewModel.kt | 74 +++++++++++++++ 3 files changed, 112 insertions(+), 78 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt index c53cf79ff..5e3c4b1b6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt @@ -36,17 +36,20 @@ class PollNoteEvent( .forEach { map[it[1].toInt()] = it[2] } return map } - fun valueMaximum(): Int? = tags.filter { it.firstOrNull() == VALUE_MAXIMUM } - .getOrNull(1)?.getOrNull(1)?.toInt() - fun valueMinimum(): Int? = tags.filter { it.firstOrNull() == VALUE_MINIMUM } - .getOrNull(1)?.getOrNull(1)?.toInt() + fun getTagInt(property: String): Int? { + val tagList = tags.filter { + it.firstOrNull() == property + } + val tag = tagList.getOrNull(0) + val s = tag?.getOrNull(1) - fun consensusThreshold(): Int? = tags.filter { it.firstOrNull() == CONSENSUS_THRESHOLD } - .getOrNull(1)?.getOrNull(1)?.toInt() - - fun closedAt(): Int? = tags.filter { it.firstOrNull() == CLOSED_AT } - .getOrNull(1)?.getOrNull(1)?.toInt() + return if (s.isNullOrBlank() || s == "null") { + null + } else { + s.toInt() + } + } companion object { const val kind = 6969 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 31c9849aa..74660f1ab 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 @@ -26,10 +26,10 @@ 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.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.service.model.PollNoteEvent import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange @@ -44,10 +44,10 @@ fun PollNote( accountViewModel: AccountViewModel, navController: NavController ) { - val pollEvent = note.event as PollNoteEvent - val consensusThreshold = pollEvent.consensusThreshold() + val pollViewModel: PollNoteViewModel = viewModel() + pollViewModel.load(note) - pollEvent.pollOptions().forEach { poll_op -> + pollViewModel.pollEvent?.pollOptions()?.forEach { poll_op -> Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth() @@ -56,16 +56,16 @@ fun PollNote( poll_op.value, canPreview, modifier = Modifier - .weight(1f) + .width(250.dp) .border(BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f))) .padding(4.dp), - pollEvent.tags(), + pollViewModel.pollEvent?.tags(), backgroundColor, accountViewModel, navController ) - ZapVote(note, accountViewModel, poll_op.key) + ZapVote(note, accountViewModel, pollViewModel, poll_op.key) } } } @@ -75,6 +75,7 @@ fun PollNote( fun ZapVote( baseNote: Note, accountViewModel: AccountViewModel, + pollViewModel: PollNoteViewModel, pollOption: Int, modifier: Modifier = Modifier ) { @@ -89,15 +90,6 @@ fun ZapVote( val context = LocalContext.current val scope = rememberCoroutineScope() - val pollEvent = baseNote.event as PollNoteEvent - val valueMaximum = pollEvent.valueMaximum() - val valueMinimum = pollEvent.valueMinimum() - - val isPollClosed: Boolean = pollEvent.closedAt()?.let { // allow 2 minute leeway for zap to propagate - baseNote.createdAt()?.plus(it * (86400 + 120))!! > Date().time / 1000 - } == true - val isVoteAmountAtomic = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum - Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier @@ -117,7 +109,7 @@ fun ZapVote( ) .show() } - } else if (isPollClosed) { + } else if (pollViewModel.isPollClosed) { scope.launch { Toast .makeText( @@ -127,10 +119,10 @@ fun ZapVote( ) .show() } - } else if (isVoteAmountAtomic) { + } else if (pollViewModel.isVoteAmountAtomic) { accountViewModel.zap( baseNote, - valueMaximum!!.toLong() * 1000, + pollViewModel.valueMaximum!!.toLong() * 1000, pollOption, "", context @@ -152,9 +144,8 @@ fun ZapVote( ZapVoteAmountChoicePopup( baseNote, accountViewModel, + pollViewModel, pollOption, - valueMinimum, - valueMaximum, onDismiss = { wantsToZap = false }, @@ -183,12 +174,14 @@ fun ZapVote( } } - Text( - showAmount(zappedNote?.zappedPollOptionAmount(pollOption)), - fontSize = 14.sp, - color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), - modifier = modifier - ) + if (zappedNote?.isZappedBy(account.userProfile()) == true) { + Text( + showAmount(zappedNote.zappedPollOptionAmount(pollOption)), + fontSize = 14.sp, + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), + modifier = modifier + ) + } } @OptIn(ExperimentalFoundationApi::class) @@ -196,9 +189,8 @@ fun ZapVote( fun ZapVoteAmountChoicePopup( baseNote: Note, accountViewModel: AccountViewModel, + pollViewModel: PollNoteViewModel, pollOption: Int, - valueMinimum: Int?, - valueMaximum: Int?, onDismiss: () -> Unit, onError: (text: String) -> Unit ) { @@ -206,43 +198,6 @@ fun ZapVoteAmountChoicePopup( var textAmount by rememberSaveable { mutableStateOf("") } - val placeHolderText = if (valueMinimum == null && valueMaximum == null) { - stringResource(R.string.sats) - } else if (valueMinimum == null) { - "1—$valueMaximum " + stringResource(R.string.sats) - } else if (valueMaximum == null) { - ">$valueMinimum " + stringResource(R.string.sats) - } else { - "$valueMinimum—$valueMaximum " + stringResource(R.string.sats) - } - - val amount = if (textAmount.isEmpty()) { null } else { - try { - textAmount.toLong() - } catch (e: Exception) { null } - } - - var isValidAmount = false - if (amount == null) { - isValidAmount = false - } else if (valueMinimum == null && valueMaximum == null) { - if (amount > 0) { - isValidAmount = true - } - } else if (valueMinimum == null) { - if (amount > 0 && amount <= valueMaximum!!) { - isValidAmount = true - } - } else if (valueMaximum == null) { - if (amount >= valueMinimum) { - isValidAmount = true - } - } else { - if ((valueMinimum <= amount) && (amount <= valueMaximum)) { - isValidAmount = true - } - } - val colorInValid = TextFieldDefaults.outlinedTextFieldColors( focusedBorderColor = MaterialTheme.colors.error, unfocusedBorderColor = Color.Red @@ -266,12 +221,14 @@ fun ZapVoteAmountChoicePopup( modifier = Modifier .padding(10.dp) ) { + val amount = pollViewModel.amount(textAmount) + OutlinedTextField( value = textAmount, onValueChange = { textAmount = it }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), modifier = Modifier.width(150.dp), - colors = if (isValidAmount) colorValid else colorInValid, + colors = if (pollViewModel.isValidAmount(amount)) colorValid else colorInValid, label = { Text( text = stringResource(R.string.poll_zap_amount), @@ -280,7 +237,7 @@ fun ZapVoteAmountChoicePopup( }, placeholder = { Text( - text = placeHolderText, + text = pollViewModel.voteAmountPlaceHolderText(context), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) } @@ -288,7 +245,7 @@ fun ZapVoteAmountChoicePopup( Button( modifier = Modifier.padding(horizontal = 3.dp), - enabled = isValidAmount, + enabled = pollViewModel.isValidAmount(amount), onClick = { if (amount != null) { accountViewModel.zap( 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 new file mode 100644 index 000000000..da9fa229a --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt @@ -0,0 +1,74 @@ +package com.vitorpamplona.amethyst.ui.note + +import android.content.Context +import androidx.lifecycle.ViewModel +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.model.* +import java.util.* + +class PollNoteViewModel : ViewModel() { + var account: Account? = null + var pollNote: Note? = null + + var pollEvent: PollNoteEvent? = null + var valueMaximum: Int? = null + var valueMinimum: Int? = null + var closedAt: Int? = null + var consensusThreshold: Int? = null + + fun load(note: Note?) { + pollNote = note + pollEvent = pollNote?.event as PollNoteEvent + valueMaximum = pollEvent?.getTagInt(VALUE_MAXIMUM) + valueMinimum = pollEvent?.getTagInt(VALUE_MINIMUM) + consensusThreshold = pollEvent?.getTagInt(CONSENSUS_THRESHOLD) + closedAt = pollEvent?.getTagInt(CLOSED_AT) + } + + val isPollClosed: Boolean = closedAt?.let { // allow 2 minute leeway for zap to propagate + pollNote?.createdAt()?.plus(it * (86400 + 120))!! > Date().time / 1000 + } == true + + val isVoteAmountAtomic = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum + + fun voteAmountPlaceHolderText(ctx: Context): String = if (valueMinimum == null && valueMaximum == null) { + ctx.getString(R.string.sats) + } else if (valueMinimum == null) { + "1—$valueMaximum " + ctx.getString(R.string.sats) + } else if (valueMaximum == null) { + ">$valueMinimum " + ctx.getString(R.string.sats) + } else { + "$valueMinimum—$valueMaximum " + ctx.getString(R.string.sats) + } + + fun amount(textAmount: String) = if (textAmount.isEmpty()) { null } else { + try { + textAmount.toLong() + } catch (e: Exception) { null } + } + + fun isValidAmount(amount: Long?): Boolean { + if (amount == null) { + return false + } else if (valueMinimum == null && valueMaximum == null) { + if (amount > 0) { + return true + } + } else if (valueMinimum == null) { + if (amount > 0 && amount <= valueMaximum!!) { + return true + } + } else if (valueMaximum == null) { + if (amount >= valueMinimum!!) { + return true + } + } else { + if ((valueMinimum!! <= amount) && (amount <= valueMaximum!!)) { + return true + } + } + return false + } +} From 5b1e7c34515db00b638cba6f8f05c783e2f539a2 Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Sat, 25 Mar 2023 09:18:13 +0900 Subject: [PATCH 31/36] don't pass in context to voteAmountPlaceHolderText() --- .../com/vitorpamplona/amethyst/ui/note/PollNote.kt | 2 +- .../amethyst/ui/note/PollNoteViewModel.kt | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) 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 74660f1ab..4a119d44f 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 @@ -237,7 +237,7 @@ fun ZapVoteAmountChoicePopup( }, placeholder = { Text( - text = pollViewModel.voteAmountPlaceHolderText(context), + text = pollViewModel.voteAmountPlaceHolderText(context.getString(R.string.sats)), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) } 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 da9fa229a..89b9bbe92 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 @@ -1,8 +1,6 @@ package com.vitorpamplona.amethyst.ui.note -import android.content.Context import androidx.lifecycle.ViewModel -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.* @@ -33,14 +31,14 @@ class PollNoteViewModel : ViewModel() { val isVoteAmountAtomic = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum - fun voteAmountPlaceHolderText(ctx: Context): String = if (valueMinimum == null && valueMaximum == null) { - ctx.getString(R.string.sats) + fun voteAmountPlaceHolderText(sats: String): String = if (valueMinimum == null && valueMaximum == null) { + sats } else if (valueMinimum == null) { - "1—$valueMaximum " + ctx.getString(R.string.sats) + "1—$valueMaximum $sats" } else if (valueMaximum == null) { - ">$valueMinimum " + ctx.getString(R.string.sats) + ">$valueMinimum $sats" } else { - "$valueMinimum—$valueMaximum " + ctx.getString(R.string.sats) + "$valueMinimum—$valueMaximum $sats" } fun amount(textAmount: String) = if (textAmount.isEmpty()) { null } else { From 12a1c3fe6d0683e6f0d7d33ea57bcd73ec2e7151 Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Sat, 25 Mar 2023 15:46:54 +0900 Subject: [PATCH 32/36] add option tally percentage bars, create new PollNoteViewModel for each PollNote, fix isVoteAmountAtomic to directly launch wallet --- .../amethyst/ui/note/PollNote.kt | 36 ++++++++++++------- .../amethyst/ui/note/PollNoteViewModel.kt | 34 +++++++++++++----- 2 files changed, 48 insertions(+), 22 deletions(-) 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 4a119d44f..0f0f9c3ab 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 @@ -26,7 +26,6 @@ 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.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note @@ -44,21 +43,19 @@ fun PollNote( accountViewModel: AccountViewModel, navController: NavController ) { - val pollViewModel: PollNoteViewModel = viewModel() + val pollViewModel = PollNoteViewModel() pollViewModel.load(note) pollViewModel.pollEvent?.pollOptions()?.forEach { poll_op -> Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() + verticalAlignment = Alignment.CenterVertically ) { TranslateableRichTextViewer( poll_op.value, canPreview, modifier = Modifier .width(250.dp) - .border(BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f))) - .padding(4.dp), + .border(BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f))), pollViewModel.pollEvent?.tags(), backgroundColor, accountViewModel, @@ -67,6 +64,18 @@ fun PollNote( ZapVote(note, accountViewModel, pollViewModel, poll_op.key) } + + Row( + verticalAlignment = Alignment.CenterVertically + ) { + // only show tallies after user has zapped note + if (note.isZappedBy(accountViewModel.userProfile())) { + LinearProgressIndicator( + modifier = Modifier.width(250.dp), + progress = pollViewModel.optionVoteTally(poll_op.key) + ) + } + } } } @@ -119,7 +128,7 @@ fun ZapVote( ) .show() } - } else if (pollViewModel.isVoteAmountAtomic) { + } else if (pollViewModel.isVoteAmountAtomic()) { accountViewModel.zap( baseNote, pollViewModel.valueMaximum!!.toLong() * 1000, @@ -174,6 +183,7 @@ fun ZapVote( } } + // only show tallies after a user has zapped note if (zappedNote?.isZappedBy(account.userProfile()) == true) { Text( showAmount(zappedNote.zappedPollOptionAmount(pollOption)), @@ -196,7 +206,7 @@ fun ZapVoteAmountChoicePopup( ) { val context = LocalContext.current - var textAmount by rememberSaveable { mutableStateOf("") } + var inputAmountText by rememberSaveable { mutableStateOf("") } val colorInValid = TextFieldDefaults.outlinedTextFieldColors( focusedBorderColor = MaterialTheme.colors.error, @@ -221,14 +231,14 @@ fun ZapVoteAmountChoicePopup( modifier = Modifier .padding(10.dp) ) { - val amount = pollViewModel.amount(textAmount) + val amount = pollViewModel.inputAmountLong(inputAmountText) OutlinedTextField( - value = textAmount, - onValueChange = { textAmount = it }, + value = inputAmountText, + onValueChange = { inputAmountText = it }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), modifier = Modifier.width(150.dp), - colors = if (pollViewModel.isValidAmount(amount)) colorValid else colorInValid, + colors = if (pollViewModel.isValidInputAmount(amount)) colorValid else colorInValid, label = { Text( text = stringResource(R.string.poll_zap_amount), @@ -245,7 +255,7 @@ fun ZapVoteAmountChoicePopup( Button( modifier = Modifier.padding(horizontal = 3.dp), - enabled = pollViewModel.isValidAmount(amount), + enabled = pollViewModel.isValidInputAmount(amount), onClick = { if (amount != null) { accountViewModel.zap( 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 89b9bbe92..2c4a04df4 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 @@ -1,24 +1,25 @@ package com.vitorpamplona.amethyst.ui.note -import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.* import java.util.* -class PollNoteViewModel : ViewModel() { +class PollNoteViewModel { var account: Account? = null - var pollNote: Note? = null + private var pollNote: Note? = null var pollEvent: PollNoteEvent? = null + private var pollOptions: Map? = null var valueMaximum: Int? = null - var valueMinimum: Int? = null - var closedAt: Int? = null - var consensusThreshold: Int? = null + private var valueMinimum: Int? = null + private var closedAt: Int? = null + private var consensusThreshold: Int? = null fun load(note: Note?) { pollNote = note pollEvent = pollNote?.event as PollNoteEvent + pollOptions = pollEvent?.pollOptions() valueMaximum = pollEvent?.getTagInt(VALUE_MAXIMUM) valueMinimum = pollEvent?.getTagInt(VALUE_MINIMUM) consensusThreshold = pollEvent?.getTagInt(CONSENSUS_THRESHOLD) @@ -29,7 +30,7 @@ class PollNoteViewModel : ViewModel() { pollNote?.createdAt()?.plus(it * (86400 + 120))!! > Date().time / 1000 } == true - val isVoteAmountAtomic = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum + fun isVoteAmountAtomic(): Boolean = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum fun voteAmountPlaceHolderText(sats: String): String = if (valueMinimum == null && valueMaximum == null) { sats @@ -41,13 +42,13 @@ class PollNoteViewModel : ViewModel() { "$valueMinimum—$valueMaximum $sats" } - fun amount(textAmount: String) = if (textAmount.isEmpty()) { null } else { + fun inputAmountLong(textAmount: String) = if (textAmount.isEmpty()) { null } else { try { textAmount.toLong() } catch (e: Exception) { null } } - fun isValidAmount(amount: Long?): Boolean { + fun isValidInputAmount(amount: Long?): Boolean { if (amount == null) { return false } else if (valueMinimum == null && valueMaximum == null) { @@ -69,4 +70,19 @@ class PollNoteViewModel : ViewModel() { } return false } + + fun optionVoteTally(op: Int): Float { + val tally = pollNote?.zappedPollOptionAmount(op)?.toFloat()?.div(zappedVoteTotal()) ?: 0f + return if (tally.isNaN()) { // catch div by 0 + 0f + } else { tally } + } + + private fun zappedVoteTotal(): Float { + var total = 0f + pollOptions?.keys?.forEach { + total += pollNote?.zappedPollOptionAmount(it)?.toFloat() ?: 0f + } + return total + } } From 584c2860e4b7a28383607e2b3e2f8abbb52b5221 Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Sun, 26 Mar 2023 09:51:00 +0900 Subject: [PATCH 33/36] move option tally logic to PollNoteViewModel --- .../com/vitorpamplona/amethyst/model/Note.kt | 25 -------------- .../amethyst/ui/actions/NewPollView.kt | 2 +- .../amethyst/ui/note/PollNote.kt | 4 +-- .../amethyst/ui/note/PollNoteViewModel.kt | 33 +++++++++++++++++-- 4 files changed, 34 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 283f47262..8437d532b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -203,20 +203,6 @@ open class Note(val idHex: String) { return zaps.any { it.key.author == user } } - fun isPollOptionZappedBy(option: Int, user: User): Boolean { - if (zaps.any { it.key.author == user }) { - zaps.mapNotNull { it.value?.event } - .filterIsInstance() - .map { - val zappedOption = it.zappedPollOption() - if (zappedOption == option) { - return true - } - } - } - return false - } - fun isReactedBy(user: User): Boolean { return reactions.any { it.author == user } } @@ -251,17 +237,6 @@ open class Note(val idHex: String) { }.sumOf { it } } - fun zappedPollOptionAmount(option: Int): BigDecimal { - return zaps.mapNotNull { it.value?.event } - .filterIsInstance() - .mapNotNull { - val zappedOption = it.zappedPollOption() - if (zappedOption == option) { - it.amount - } else { null } - }.sumOf { it } - } - fun hasAnyReports(): Boolean { val dayAgo = Date().time / 1000 - 24 * 60 * 60 return reports.isNotEmpty() || 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 275948e69..f16623cce 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 @@ -114,7 +114,7 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n } Text(stringResource(R.string.poll_heading_required)) - NewPollRecipientsField(pollViewModel, account) + // NewPollRecipientsField(pollViewModel, account) NewPollPrimaryDescription(pollViewModel) pollViewModel.pollOptions.values.forEachIndexed { index, element -> NewPollOption(pollViewModel, index) 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 0f0f9c3ab..3fc8cc7d3 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 @@ -166,7 +166,7 @@ fun ZapVote( ) } - if (zappedNote?.isPollOptionZappedBy(pollOption, account.userProfile()) == true) { + if (pollViewModel.isPollOptionZappedBy(pollOption, account.userProfile())) { Icon( imageVector = Icons.Default.Bolt, contentDescription = stringResource(R.string.zaps), @@ -186,7 +186,7 @@ fun ZapVote( // only show tallies after a user has zapped note if (zappedNote?.isZappedBy(account.userProfile()) == true) { Text( - showAmount(zappedNote.zappedPollOptionAmount(pollOption)), + showAmount(pollViewModel.zappedPollOptionAmount(pollOption)), fontSize = 14.sp, color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), modifier = modifier 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 2c4a04df4..d97d5219e 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 @@ -2,7 +2,9 @@ package com.vitorpamplona.amethyst.ui.note import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.model.* +import java.math.BigDecimal import java.util.* class PollNoteViewModel { @@ -72,7 +74,7 @@ class PollNoteViewModel { } fun optionVoteTally(op: Int): Float { - val tally = pollNote?.zappedPollOptionAmount(op)?.toFloat()?.div(zappedVoteTotal()) ?: 0f + val tally = zappedPollOptionAmount(op).toFloat().div(zappedVoteTotal()) return if (tally.isNaN()) { // catch div by 0 0f } else { tally } @@ -81,8 +83,35 @@ class PollNoteViewModel { private fun zappedVoteTotal(): Float { var total = 0f pollOptions?.keys?.forEach { - total += pollNote?.zappedPollOptionAmount(it)?.toFloat() ?: 0f + total += zappedPollOptionAmount(it).toFloat() ?: 0f } return total } + + fun isPollOptionZappedBy(option: Int, user: User): Boolean { + if (pollNote?.zaps?.any { it.key.author == user } == true) { + pollNote!!.zaps.mapNotNull { it.value?.event } + .filterIsInstance() + .map { + val zappedOption = it.zappedPollOption() + if (zappedOption == option) { + return true + } + } + } + return false + } + + fun zappedPollOptionAmount(option: Int): BigDecimal { + if (pollNote != null) { + return pollNote!!.zaps.mapNotNull { it.value?.event } + .filterIsInstance() + .mapNotNull { + val zappedOption = it.zappedPollOption() + if (zappedOption == option) { + it.amount + } else { null } + }.sumOf { it } + } else { return BigDecimal(0) } + } } From 62ff9ac94bd9f04115eb8c9f0d259f042f6f551f Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Sun, 26 Mar 2023 13:52:27 +0900 Subject: [PATCH 34/36] color option progress bar green when consensus achieved, add padding to option text --- .../amethyst/ui/note/PollNote.kt | 22 ++++++++++++++----- .../amethyst/ui/note/PollNoteViewModel.kt | 20 +++++++++-------- 2 files changed, 27 insertions(+), 15 deletions(-) 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 3fc8cc7d3..ef3f4a14c 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 @@ -47,6 +47,8 @@ fun PollNote( pollViewModel.load(note) pollViewModel.pollEvent?.pollOptions()?.forEach { poll_op -> + val optionTally = pollViewModel.optionVoteTally(poll_op.key) + Row( verticalAlignment = Alignment.CenterVertically ) { @@ -55,7 +57,9 @@ fun PollNote( canPreview, modifier = Modifier .width(250.dp) - .border(BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f))), + .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, @@ -72,7 +76,13 @@ fun PollNote( if (note.isZappedBy(accountViewModel.userProfile())) { LinearProgressIndicator( modifier = Modifier.width(250.dp), - progress = pollViewModel.optionVoteTally(poll_op.key) + color = if ( + pollViewModel.consensusThreshold != null && + optionTally >= pollViewModel.consensusThreshold!! + ) { + Color.Green + } else { MaterialTheme.colors.primary }, + progress = optionTally ) } } @@ -128,7 +138,7 @@ fun ZapVote( ) .show() } - } else if (pollViewModel.isVoteAmountAtomic()) { + } else if (pollViewModel.isVoteAmountAtomic) { accountViewModel.zap( baseNote, pollViewModel.valueMaximum!!.toLong() * 1000, @@ -231,14 +241,14 @@ fun ZapVoteAmountChoicePopup( modifier = Modifier .padding(10.dp) ) { - val amount = pollViewModel.inputAmountLong(inputAmountText) + val amount = pollViewModel.inputVoteAmountLong(inputAmountText) OutlinedTextField( value = inputAmountText, onValueChange = { inputAmountText = it }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), modifier = Modifier.width(150.dp), - colors = if (pollViewModel.isValidInputAmount(amount)) colorValid else colorInValid, + colors = if (pollViewModel.isValidInputVoteAmount(amount)) colorValid else colorInValid, label = { Text( text = stringResource(R.string.poll_zap_amount), @@ -255,7 +265,7 @@ fun ZapVoteAmountChoicePopup( Button( modifier = Modifier.padding(horizontal = 3.dp), - enabled = pollViewModel.isValidInputAmount(amount), + enabled = pollViewModel.isValidInputVoteAmount(amount), onClick = { if (amount != null) { accountViewModel.zap( 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 d97d5219e..7e5da1765 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 @@ -16,7 +16,7 @@ class PollNoteViewModel { var valueMaximum: Int? = null private var valueMinimum: Int? = null private var closedAt: Int? = null - private var consensusThreshold: Int? = null + var consensusThreshold: Float? = null fun load(note: Note?) { pollNote = note @@ -24,7 +24,7 @@ class PollNoteViewModel { pollOptions = pollEvent?.pollOptions() valueMaximum = pollEvent?.getTagInt(VALUE_MAXIMUM) valueMinimum = pollEvent?.getTagInt(VALUE_MINIMUM) - consensusThreshold = pollEvent?.getTagInt(CONSENSUS_THRESHOLD) + consensusThreshold = pollEvent?.getTagInt(CONSENSUS_THRESHOLD)?.toFloat()?.div(100) closedAt = pollEvent?.getTagInt(CLOSED_AT) } @@ -32,7 +32,7 @@ class PollNoteViewModel { pollNote?.createdAt()?.plus(it * (86400 + 120))!! > Date().time / 1000 } == true - fun isVoteAmountAtomic(): Boolean = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum + val isVoteAmountAtomic = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum fun voteAmountPlaceHolderText(sats: String): String = if (valueMinimum == null && valueMaximum == null) { sats @@ -44,13 +44,13 @@ class PollNoteViewModel { "$valueMinimum—$valueMaximum $sats" } - fun inputAmountLong(textAmount: String) = if (textAmount.isEmpty()) { null } else { + fun inputVoteAmountLong(textAmount: String) = if (textAmount.isEmpty()) { null } else { try { textAmount.toLong() } catch (e: Exception) { null } } - fun isValidInputAmount(amount: Long?): Boolean { + fun isValidInputVoteAmount(amount: Long?): Boolean { if (amount == null) { return false } else if (valueMinimum == null && valueMaximum == null) { @@ -83,7 +83,7 @@ class PollNoteViewModel { private fun zappedVoteTotal(): Float { var total = 0f pollOptions?.keys?.forEach { - total += zappedPollOptionAmount(it).toFloat() ?: 0f + total += zappedPollOptionAmount(it).toFloat() } return total } @@ -103,8 +103,8 @@ class PollNoteViewModel { } fun zappedPollOptionAmount(option: Int): BigDecimal { - if (pollNote != null) { - return pollNote!!.zaps.mapNotNull { it.value?.event } + return if (pollNote != null) { + pollNote!!.zaps.mapNotNull { it.value?.event } .filterIsInstance() .mapNotNull { val zappedOption = it.zappedPollOption() @@ -112,6 +112,8 @@ class PollNoteViewModel { it.amount } else { null } }.sumOf { it } - } else { return BigDecimal(0) } + } else { + BigDecimal(0) + } } } From b65139f52074dff176147893bb681caf849f9c4d Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Sun, 26 Mar 2023 14:34:32 +0900 Subject: [PATCH 35/36] only allow one vote per option on atomic (main==max) polls fix buggy click event on disabled zap button --- .../amethyst/ui/note/PollNote.kt | 22 ++++++++++++++----- .../amethyst/ui/note/PollNoteViewModel.kt | 4 ++-- app/src/main/res/values/strings.xml | 1 + 3 files changed, 19 insertions(+), 8 deletions(-) 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 ef3f4a14c..148c2e1ef 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 @@ -138,7 +138,16 @@ fun ZapVote( ) .show() } - } else if (pollViewModel.isVoteAmountAtomic) { + } 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 + } accountViewModel.zap( baseNote, pollViewModel.valueMaximum!!.toLong() * 1000, @@ -263,11 +272,12 @@ fun ZapVoteAmountChoicePopup( } ) + val isValidInputAmount = pollViewModel.isValidInputVoteAmount(amount) Button( modifier = Modifier.padding(horizontal = 3.dp), - enabled = pollViewModel.isValidInputVoteAmount(amount), + enabled = isValidInputAmount, onClick = { - if (amount != null) { + if (amount != null && isValidInputAmount) { accountViewModel.zap( baseNote, amount * 1000, @@ -276,8 +286,8 @@ fun ZapVoteAmountChoicePopup( context, onError ) + onDismiss() } - onDismiss() }, shape = RoundedCornerShape(20.dp), colors = ButtonDefaults @@ -291,7 +301,7 @@ fun ZapVoteAmountChoicePopup( textAlign = TextAlign.Center, modifier = Modifier.combinedClickable( onClick = { - if (amount != null) { + if (amount != null && isValidInputAmount) { accountViewModel.zap( baseNote, amount * 1000, @@ -300,8 +310,8 @@ fun ZapVoteAmountChoicePopup( context, onError ) + onDismiss() } - onDismiss() }, onLongClick = {} ) 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 7e5da1765..77080d3e0 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 @@ -28,12 +28,12 @@ class PollNoteViewModel { closedAt = pollEvent?.getTagInt(CLOSED_AT) } + fun isVoteAmountAtomic() = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum + val isPollClosed: Boolean = closedAt?.let { // allow 2 minute leeway for zap to propagate pollNote?.createdAt()?.plus(it * (86400 + 120))!! > Date().time / 1000 } == true - val isVoteAmountAtomic = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum - fun voteAmountPlaceHolderText(sats: String): String = if (valueMinimum == null && valueMaximum == null) { sats } else if (valueMinimum == null) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0fa1498cc..c73475bc1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -235,5 +235,6 @@ days Poll is closed to new votes Zap amount + Only one vote per user is allowed on this type of poll From 573cfa8b759ee80984edf01774353ec32c319105 Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Tue, 28 Mar 2023 14:09:07 +0900 Subject: [PATCH 36/36] add expandable FAB compose menu, increase bolt size of poll icon --- .../amethyst/ui/buttons/FabColumn.kt | 95 ++++++++++++++++-- .../amethyst/ui/buttons/NewNoteButton.kt | 47 --------- .../amethyst/ui/buttons/NewPollButton.kt | 47 --------- .../amethyst/ui/screen/loggedIn/MainScreen.kt | 2 +- app/src/main/res/drawable-hdpi/ic_poll.png | Bin 655 -> 2647 bytes app/src/main/res/drawable-mdpi/ic_poll.png | Bin 402 -> 1574 bytes app/src/main/res/drawable-xhdpi/ic_poll.png | Bin 822 -> 3400 bytes app/src/main/res/drawable-xxhdpi/ic_poll.png | Bin 1348 -> 5488 bytes app/src/main/res/drawable-xxxhdpi/ic_poll.png | Bin 2072 -> 2833 bytes 9 files changed, 88 insertions(+), 103 deletions(-) delete mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewNoteButton.kt delete mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewPollButton.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/FabColumn.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/FabColumn.kt index 69601ece1..1a0715ef2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/FabColumn.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/FabColumn.kt @@ -1,18 +1,97 @@ -package com.vitorpamplona.amethyst.buttons +package com.vitorpamplona.amethyst.ui.buttons -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.runtime.Composable +import androidx.compose.foundation.layout.* +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.* 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.service.NostrAccountDataSource +import com.vitorpamplona.amethyst.ui.actions.NewPollView +import com.vitorpamplona.amethyst.ui.actions.NewPostView @Composable fun FabColumn(account: Account) { + var isOpen by remember { + mutableStateOf(false) + } + var wantsToPoll by remember { + mutableStateOf(false) + } + var wantsToPost by remember { + mutableStateOf(false) + } + Column() { - NewPollButton(account) - Spacer(modifier = Modifier.height(20.dp)) - NewNoteButton(account) + if (isOpen) { + OutlinedButton( + onClick = { + wantsToPoll = true + isOpen = false + }, + modifier = Modifier.size(45.dp), + shape = CircleShape, + colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.primary), + contentPadding = PaddingValues(0.dp) + ) { + Icon( + painter = painterResource(R.drawable.ic_poll), + null, + modifier = Modifier.size(26.dp), + tint = Color.White + ) + } + + Spacer(modifier = Modifier.height(20.dp)) + + OutlinedButton( + onClick = { + wantsToPost = true + isOpen = false + }, + modifier = Modifier.size(45.dp), + shape = CircleShape, + colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.primary), + contentPadding = PaddingValues(0.dp) + ) { + Icon( + painter = painterResource(R.drawable.ic_lists), + null, + modifier = Modifier.size(26.dp), + tint = Color.White + ) + } + + Spacer(modifier = Modifier.height(20.dp)) + } + OutlinedButton( + onClick = { isOpen = !isOpen }, + 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 + ) + } + } + + if (wantsToPost) { + NewPostView({ wantsToPost = false }, account = NostrAccountDataSource.account) + } + + if (wantsToPoll) { + NewPollView({ wantsToPoll = false }, account = account) } } 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 deleted file mode 100644 index b16534b28..000000000 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewNoteButton.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.vitorpamplona.amethyst.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/buttons/NewPollButton.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewPollButton.kt deleted file mode 100644 index 5169f5b1e..000000000 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewPollButton.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.vitorpamplona.amethyst.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.NewPollView - -@Composable -fun NewPollButton(account: Account) { - var wantsToPoll by remember { - mutableStateOf(false) - } - - if (wantsToPoll) { - NewPollView({ wantsToPoll = false }, account = account) - } - - OutlinedButton( - onClick = { wantsToPoll = 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_poll), - null, - modifier = Modifier.size(26.dp), - tint = Color.White - ) - } -} 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 46b55c10c..c56340d8f 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 @@ -13,8 +13,8 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController -import com.vitorpamplona.amethyst.buttons.FabColumn import com.vitorpamplona.amethyst.buttons.NewChannelButton +import com.vitorpamplona.amethyst.ui.buttons.FabColumn import com.vitorpamplona.amethyst.ui.navigation.* import com.vitorpamplona.amethyst.ui.screen.AccountState import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel diff --git a/app/src/main/res/drawable-hdpi/ic_poll.png b/app/src/main/res/drawable-hdpi/ic_poll.png index ff8df419e575bb9dbf77c43618fff3f955989987..560b632f28b569bb8389cfb2753b282da4db3600 100644 GIT binary patch literal 2647 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 FV1iH?{?Gsb literal 655 zcmV;A0&x9_P)0S zd)l2kE*SfknB7Akc$wtgo#*}S%+74GrBadQoI4jqQI&`%6jjyCa_p+x?cU_z`&O&< zlM{C)5vrw{s{IFyC_N1Ena^)05vt{(TsTtDRy|C!*_;hn|EpBXLy2MkK(sfXJE9KG zpNLd5ox*rOc?1+FH=wk}Qm50o8s-lJG)4ZTHHO3Cac`jS_`jk3;4=G;vj8PYa*W)N zV;`+SzqLxGa$Y{|c6*-OHn|Pj71}$#h6xD6Q$Yt)&~M965A6k(_7ieHXdi3Dd<_$j z)bIB%W8&B|B1_ zt=8-Himzb;(oKl*>$HO@=(|D8$fwuqoyF$?-@mg0mvx~*je`S88h*^m)<|J?m%Vmv z{8xzOa0i#nK;w`$$VQ{F5RQyjEk_kvK1Eq0K=>ysYL^3kY}yH9S5 zDO)W4e(2hOr1U4qjQ$~5LfeJ+(1kHNFS0O_8E705Ak$aR=A~SKM(GIXU-}zRvHnWC pqP=X83iUe1OO#PjURV`HzX0urVIKF}q~!nr002ovPDHLkV1n9nIF|qb diff --git a/app/src/main/res/drawable-mdpi/ic_poll.png b/app/src/main/res/drawable-mdpi/ic_poll.png index a0282c9f3f920bc658ee1900253cfdc54c014a06..6a27ba8a78ce6142fac4f366149388fd14dfde2f 100644 GIT binary patch literal 1574 zcmV+>2HE+EP)}9w8tyX&m0MzUC_jx@8UTAAdX|o82j81#uzB2;G9dXwH`rPE|=5g za`~-hv-wWFUVpDvtNq*nfGCRA9LGVLroWa_9#TpnrG%6cuTyfZH4MYRa5%*2=_!2Q zhi%&sLO@ChAq0dFZO3umiK1u?7@aX=>^UI>VHj2n!#I>u!Zb}33I!;orh-k=L>PwX z^?Cr1p_KkfYwZG{rfI$_g!o?&1QkLE7-P@RzyLG=7=}SP=df)XtE;QvoS!MWEGVTY zm&?dxG9ZMk4~N4aa?ZgRLpGcJUYe!=V2(s+0H7}>Nz)XIi;F0gN>ECHF*fznT4R2G z9**PS*|TRGBV|xZ-HW1#2_Y(k5PqIxoL@*Ok32O?126)q{hBpJ}Zzt;3a4=yW=GW9f}Q zx~_X)2yu%LazksK_kF*ml=@2$1l3o94Z}bfhKQmFg+k%}#>U2*`F!4&QeJw-FZ(PsjwAF@`iv(P%WVxVVT)rSdP&^Dt_T zSB7C27z_qzG#W4r1B|gBc%Fwwqw(##cklM^+_{5Nsgy=h^m4G)8iWvV&XLV#(d~8- z1i{p<)>@-bC?E&|?ACWN_qDlOCnqPr34-9iSFc`$@B2uSWOB1FCQ(XZd3pIP(YR|b zi}BoVUf+ZU5J%B>_xJbfwr%6ajT=x(UGRemFy|be=b>0E;^^q;TC3IikPuQJguJY) z2}U!il%iT|zEP`ve`Dj?$Hzy<^?W{$PNxGYC6rPZ9Pol-LI?;UNYgYo91hngrJ(e} zxL>G}loEqDcH%ff7>1yfBAd-3NfH=_aXHvDO$0%JZnq29b^m?+`t^+Gc?Hh-rM0G% z5&+MSkB%N`O=iQNry8u3&s+dxWX0wSaSFWH?C?tkq98WuA(=?GJ3HJB* zQ7VIG5J?qa;b1 zqfJb>&;(d11xhKFmzQB#76yYsi81DBt*2UME|-fdmCE6hCr`ea%jL$K{d$_F{{irO z#@Ihz3Fe$*W@ZMtTn>kahi~rg?mirO?xkv#QfReWSXf-xD3wYFLWp4G@k^FvEfGQ< za?U4R^8A8QiZo3jr37Q_xL7PcG)=Q`DRmTD>nNMarnYVWRw?zVQtCq~C92ixO|A9L z-rnA=^PYdftNZ;vI-L&Y=H_x#i(@NzH!SeAu$yA1$XSXg)+v7nTK5Q3eZ9fV;B+qQoK;C`pm`E!yauq+Eo zsfi99g9!i#A(UYlNYfNgpFTyu-ydJ-Q^A~bgkcD!6fDd7X|vh-#-OLioc5Z YKXP~=+CSadCIA2c07*qoM6N<$f~Y|O0ssI2 literal 402 zcmV;D0d4+?P)g-0b2Q9cKn-e9vougYlh4b7 zW5}SZYJo?aReGF4PO>fwt_EqE9_D#I)VbBb_x(NOLlY?IdET}NE`TL@lA8_i`wF#B z8!-7;_}6%-ANmwl15O6=i3t!phUgaiqbQm-T2a%`tI^TA0Zx+DR%|u^;S{xd6VQLh zH3|O_oaZ12M$D}S$j?D|wcrJu@4ZX^6R^wNYS1+BkjGk_(YirV6gy!U?(6(727SI6 zCdkBnHsiJBcXOl3Rs(6+_^|50)MwIvi~Jp{N-F%{4bXUi2`^3H-t~VzPH+MKMDwi% wO#@~})x33sdTbb|t}mRp>npSKOTWwb0_RB!0gk3V*#H0l07*qoM6N<$g6$HvQvd(} diff --git a/app/src/main/res/drawable-xhdpi/ic_poll.png b/app/src/main/res/drawable-xhdpi/ic_poll.png index 83191fa48aafe1b44100ed1e3bf225fa154d925d..02383e0b9b88259a910994da475b5bee7d60a94a 100644 GIT binary patch literal 3400 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 AUH||9 diff --git a/app/src/main/res/drawable-xxhdpi/ic_poll.png b/app/src/main/res/drawable-xxhdpi/ic_poll.png index b792c3025abe58e1277fc34fe0fa9b941090000d..3dae6049dc363ef9ffeaab4748e6c3dd629de490 100644 GIT binary patch literal 5488 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 Dw?mVS literal 1348 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 literal 2072 zcmchY`9IX#8^Awgna0{R5i@p0-68i&V@j^EW}U2qv6iJUx?DwRBD*Y8s6hsWF=b~Y zYu6If6e)9EGbAHlOU4>uEV<)*zpuOe1NVpLJg?Vt&Uu~ZInVOGV~0K`29^T@03c>* zVd}us*Y^b^#9L4MAqIKsV5o&#H~@%9?F%39kaPq9gkM{l8aZ7jEs)OR7#J8WW}vRX z*q1`j5=qUqE7kB1Lc@y1kV@~X8aggW8N)Enq3nk4rqByATBHb{9Qh~Z7%Z)9A?{^a zgubm3w9UeMpVmFgxwZ zyvef(sA2@)LkGN1EhfPs;F1ScM=g)H4+dVc*pM`y_ILBLR{JiEgOAE7 zV^I-YI!(1K5WV=+!SB2Pe=O^=ZVSnZ9KiUz7FDsXnLO*EmE+|66|2F%@I; zAvY{)o(u))t#aEqsto(uVh#1Qo=>2U&ATFGNNUfg6rQ1A1o>mUNMhGzs8qSIr%UqH zEa(GpW1ks#QOwZP@e}U4iF~OCnhFZdC^eb2AQ2PvcJ!$sHo^&_lmEY zwIXNrP5o@ydN5}}((}Xv^AEo@!)UUOj*g0tJeKGIR6|=^+glxBq={dX9HPihCo-7w z)WaTCn7b`&T%YULH?JgLQ@QE4trv+`J9CXDDc`#JZOpc|9K=IqqR5CF7XtgWXVI(yPE2-T@@kq>n2C`yx4v^NPt z-IE~FXal$A&WUY`u1hj%g^srotwDTf_L8EweAH(bF%70_^v*L;jUf%CALYi}S=czfm`O{P0(QfgsXV?9Z z2J@RRPY;hKui*w{;$FHPq$6l>1B#KCd&Kw7C0ol#l46!QzFZtI-kV0dt9UoB>;}68 zMKn9Px3XPC85_AQcI!zXqK02m2|;@*h|&b#2LuG7wP50gP^kV4{UP(YzAK{v3A8P_ z#CRDUPc$!4hzypJgY7)Pu4N}xiv>QE*10;_9dk;lra`0_DI*K6L8sr;`~1Z=6bbv5 zC;3H_yoRzqmVp1BUe7V*^yTSnM1I>H8ulI!k+WqQJXS-YAl&B1k6#VG+-#z7E{rG5 zN~xpI%EDkfV&a=KT1G!Kt-Kvw!jE6MX#~Q3$3JRvimxk7TL~0ylBst#M})n4W^<5i zt#`4GDTTb3MUS`BSYxd`Cq$}|S-NK68X`6UNHGe-63las;Ya9SZpq_UKJ|wok;=$|??tS1aYwi9MZoJoS6Gtn zk*Glac6mrVbf%Yv&$DC2#u>2ZSyY2@$3SHO6!@D-RsW7wll-yyNRwjsH84$M?s@ zq)sM$(g#Nj(?+Ci3jNuX|IRDCN&QFb|1}fbJ9(2QS|)B@O#M=0oS8km)ziivGVG*0 zcoVxjjed(0w9Z=Vn!?IpTwDz29wADFaeLDQMG+)#?!fMt5)Ue`qGhOX>)gCNuF#U- zl09Tr^c33PFyl@ayzICv{tsT&Q>-!xN(p^$>f4As zx5f&a_w;M>4NLC*gMjmm<|Zx4lSmUtPraJuNtMi3^$dqs8Px)+!ov9cm;VPRFCAQH XL4jOYQluA&hkC%$3~gF%>=XAp1kC9N