From 206d6c68bb06deec62f3bba96218587dfe43a590 Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Sat, 11 Mar 2023 15:13:02 +0900 Subject: [PATCH 001/145] 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 522bb99392..a4ddf07ffb 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 878ecefcdf..7a512c1689 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 0000000000..f4e67fac6d --- /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 0000000000..f9b72569e1 --- /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 0000000000..5923caaabb --- /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 fa698b2759..39f861f590 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 zcmV-K1-tr*P)l>Z>txJIr0QU* zQB8TVEoAJ8-A#=-qu~Yk`xlO1!CU!!{#q@OF)=n3V;Ju!HITYQ;GiYOoUiz@Bc)Pl zskHsy!I&7EN`O!xj!C`JHUdX>O&s!8Es7I@v8hbbyV;Pmi=d|R$e5`pZE6jwyXgYm zlQt3jV4YyKK-56mL~sH*fu_I-lmaJE3ak^Ty}f-oO9ry1r)N!9SJz5uqr*CZxcI|j z?-e)M|0jTrMwUYG*+Ny{uBH%$Gh8IJX{tN7Vqb z=SpJnS(kx8uxWrJCt&|53!<%uXw!!n^)bFQ!se4dDbSsL0FLdsT&^u{JTfX!b93`z zIC@<)V4P@koB>OJAjAcnn~1Uh1`fvU_ZSt3DGk1m4w)vaZXC?WAuahVgss6d3;S1G~%rXOq_7$pKe z+u7N>)84X_K5Fl+NvJz-c z0HPG(_cI)CAhXxX^Kr0BAY{yLPU&ggN{3Yf%>+)M6j&z^d~RU<#n}vt=pEozX`{nB zfmrrnEE-efJ-%boMu&9*ah(+UkH4QIhDK?lBNKttVzJ?0h)R6}DLviD zn{n=O=s(v&(ncb0S|Hf9aNU99yDW&d-hzGH`jEbGnrGMYCo|{?jsqBn`oTjOm)6N^ zj0!|2l{k6>#)-hc^X`uWr?&Wj0(iM2#y~1v?oF`;19I-l;f53hY;2L2WCz+aR{RV5!}*bb<4?- zXQunuj#UEXa=9(=xiPI<>99(mnZOB@0_y}~C69Ga2mH@tpf1GS9@1f*KrBN-{~^$4 zh;>lf=&(v4FSreLUK8j61llO=bYvn>OH0cNrgj$O23WQA4QDa<{WA1_PE_-%_IV{5 zF-o*|nc@rP>D>XYz~wbY1ww#xInx|4oB8w0Bwc+9+QMPtsJP8II-_`@p&Nl z^Pb``%i_9~j=X7s<`S4a(1*K<@3{V{)~$3{C6L;a;wzU^b{ng~DuLMb)XbXLt#nu= z&`jV2N`Vt71x}z8IDt~|R|2V`&T4_EfwYNGa4+>Hztrow*TR!u3um3uEV<)*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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f6d729ed9a..fee140f6e3 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 002/145] ignore .idea/deploymentTargetDropDown --- .gitignore | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index f928a53806..e1ddbb545e 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 003/145] ignore .idea/deploymentTargetDropDown, remove duplicate ignore rules --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e1ddbb545e..829a0c671c 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 004/145] 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 0000000000..69601ece1f --- /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 39f861f590..46b55c10cb 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 005/145] 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 f4e67fac6d..05a7c77642 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 f9b72569e1..39b43f96b7 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 1df19f6c3a..98feff7237 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 5923caaabb..5169f5b1e0 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 fee140f6e3..5a4a6a4e87 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 006/145] 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 05a7c77642..9b3f18bec6 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 007/145] 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 9b3f18bec6..1db3668ea2 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 0000000000..ca335ecbb2 --- /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 0000000000..8870b42ecd --- /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 5a4a6a4e87..db4332bdcc 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 008/145] 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 1db3668ea2..7f142094a7 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 ca335ecbb2..888c23cdf7 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 8870b42ecd..6f65c8266d 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 0000000000..0eb3b00c03 --- /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 db4332bdcc..17ed0b534d 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 009/145] 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 7f142094a7..06727d56f1 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 888c23cdf7..2a935379d7 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 0eb3b00c03..ed135f09ca 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 0000000000..b4064d3797 --- /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 17ed0b534d..6639bf010b 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 010/145] 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 06727d56f1..881666593e 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 0000000000..6debc95ff9 --- /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 b4064d3797..c4240b2374 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 6639bf010b..c4bda6c0a5 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 011/145] 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 881666593e..79910b275f 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 0000000000..2694e0ff20 --- /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 6debc95ff9..67296bd94b 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 c4240b2374..63d8881eb8 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 c4bda6c0a5..bba5e21917 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 012/145] 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 79910b275f..3cd3caf284 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 39b43f96b7..e13bc4ca8a 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 2a935379d7..ab2b1d134a 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 013/145] 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 3cd3caf284..6e523eaf18 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 e13bc4ca8a..4027994faf 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 ab2b1d134a..c6132114ab 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 ed135f09ca..6cf29d4a0c 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 014/145] 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 6e523eaf18..07067a2d51 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 4027994faf..c90562fc94 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 2694e0ff20..c3dd9251c8 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 67296bd94b..2920d00b97 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 c6132114ab..df9533e9ab 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 6f65c8266d..e68896f400 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 6cf29d4a0c..131821727d 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 63d8881eb8..9e91602567 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 bba5e21917..2a5c1bdfd6 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 015/145] 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 0000000000..9c910477dd --- /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 016/145] 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 70b237e2d1..e83d1eb5df 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 9c910477dd..6cb15d4a91 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 07067a2d51..ee54ae23b5 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 c90562fc94..e54c2107f4 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 131821727d..602a1c1afd 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 017/145] 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 9645fc1805..8b0c9502d0 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 6cb15d4a91..f6409aaacf 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 ee54ae23b5..c3c768b31e 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 e54c2107f4..2259cc83e8 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 98feff7237..d80a5b07b6 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 c3dd9251c8..9d4e45f3f8 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 2920d00b97..f60a11e772 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 9e91602567..fa6f93c62b 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 018/145] 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 c3c768b31e..22fa9d3efe 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 2259cc83e8..5b5b637e9a 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 9d4e45f3f8..c35b5faa7a 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 f60a11e772..bb8e3d42aa 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 df9533e9ab..441c711426 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 602a1c1afd..4498d2ad6a 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 fa6f93c62b..598290bf70 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 019/145] 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 8b0c9502d0..5276a14cd2 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 f6409aaacf..91215db12e 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 22fa9d3efe..ddab2e4fa2 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 5b5b637e9a..3119d65c42 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 441c711426..0e275ecc71 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 020/145] 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 3119d65c42..2f256b8b31 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 021/145] 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 91215db12e..db06080760 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 c35b5faa7a..ec4b7d6310 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 bb8e3d42aa..21e346f43d 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 0e275ecc71..c96af2a237 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 e68896f400..1084cf41c3 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 4498d2ad6a..c8aac5cfcd 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 ddab2e4fa2..275948e693 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 2f256b8b31..b4c7f5f4c2 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 598290bf70..3c4ec09082 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 022/145] 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 5276a14cd2..ca7ae2b0e4 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 f12e48acce..30f8479f95 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 9df9931af7..d92f84908f 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 440744fe82..19b815453d 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 023/145] 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 30f8479f95..d6cf919bd7 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 fac4279b97..5bd2a4e098 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 4a8830deae..dbcb987e63 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 6a07042f92..b49083b962 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 63d601b166..b5ee75889d 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 39c85a97be..6b18cb9503 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 5eda2e560e..2a477a5037 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 8b2101ce16..832fe72998 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 45b220dd96..72030d8e8b 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 707d93a50c..4990d292d6 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 44a958db5c..c006f2cace 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 024/145] 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 db06080760..32e7e59c13 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 c006f2cace..6806c7bd0b 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 0000000000..5df027a2bd --- /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 025/145] 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 6806c7bd0b..fa40d1f7d1 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 5df027a2bd..a740187189 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 fc40ed0ad8..a9adaf72ee 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 026/145] 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 32e7e59c13..c53cf79fff 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 b4c7f5f4c2..2bd10b6a20 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 a740187189..15f046029d 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 027/145] 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 ca7ae2b0e4..0daa97f587 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 b8845b8d54..a60118eaf3 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 cc95d34b32..c6ca7efb30 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 75ce41ca29..4bf0f0d115 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 fa40d1f7d1..6497fa261d 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 15f046029d..6211017ece 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 b955120a74..2bd06d01c4 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 a9adaf72ee..7322ada1b0 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 2734c6ee19..50fc500ccf 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 028/145] 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 8437d532b2..32cc0563ff 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 a60118eaf3..dd3a9bcf98 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 3c4ec09082..63a0757591 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 6211017ece..c1611a41f0 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 2a5c1bdfd6..0fa1498ccd 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 029/145] 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 32cc0563ff..283f472627 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 c1611a41f0..31c9849aae 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 030/145] 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 c53cf79fff..5e3c4b1b68 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 31c9849aae..74660f1ab0 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 0000000000..da9fa229ab --- /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 031/145] 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 74660f1ab0..4a119d44fc 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 da9fa229ab..89b9bbe926 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 032/145] 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 4a119d44fc..0f0f9c3abb 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 89b9bbe926..2c4a04df44 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 033/145] 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 283f472627..8437d532b2 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 275948e693..f16623cce5 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 0f0f9c3abb..3fc8cc7d30 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 2c4a04df44..d97d5219e3 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 034/145] 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 3fc8cc7d30..ef3f4a14c3 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 d97d5219e3..7e5da17657 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 035/145] 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 ef3f4a14c3..148c2e1ef4 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 7e5da17657..77080d3e01 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 0fa1498ccd..c73475bc15 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 036/145] 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 69601ece1f..1a0715ef29 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 b16534b28f..0000000000 --- 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 5169f5b1e0..0000000000 --- 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 46b55c10cb..c56340d8f6 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 zcmV-d3aIsoP)}U%#s8ZL0I1jN5JDi1W2~&Kl;b%5 zOt;&u5s?OPJ9b6ZT3##`S3S@B<@EIQn&){?N?~DPVGFbn0*>QgFc=^V!_UWY{AH!o zBoUe02%3dqxSk{luhnW#IF2)VC;-4(i@{)kdcFR~R;zWEF$N(7i0C%p-LfI3J2p2r ze`t*P_=yuI9uz{n33`2feX`YRJ-o8AayFmOgNQbT5HA>G#O(sj7*j-a+FColva)j4 z^SsBZ)#}fW9Xqxz0AOux?em>Z=TXi%a=F~v?Ck6Zo6Tmu*XtqM-GAGJ5U|!lDfN~U zB_b%L;CUX7967RZ`SRtzq-k32bUKeR#w>s*832S~IIWZ#cN_K7QNlS#9LKqC zw~JgZ2gVoxKq)nP*+I=(3&t3tD8lCECfe;brl+Ub`1tsAnx-iL0zk%?YebZ4t#Reb z6*!KQ8*VHiqU3tecI&+sj4^UuceI#N3Y>EUK>#AcLG!v00)xQ-mo8lbV+>PMQz#aT z-&RU}+gcj~U;uKQ^G^`bvj8x}LB<#WutS{Km9ScCSZi_O#0eN<5Jk~}IZi|nLSSNI z0-Kwg@H`KL!Qg=?id@dQo1H|dRC?VQgMPmcW6ZXvyCVW3LXsqynVCVgTE+76GNhCT z_OvkurBVr==V4=g176+(5zS?BGd#t=L{YSCt%bFAM>_5ZT5FA5E{B?jEog@inXJ>Kf(4j04 z4~1d)Nyl*n0J9rDZ1EGr-(~J1V+<8b#b@t)*PY9))z&>k#Iutd(&LMau`dwO1b}Z^ zYrnT=(AHWwj)Ov>fG`XprF0sN#$Pu#H;-RmPWNkRnj((l)gTC_NP^Zvxd z#0R95?M|n&ZLQcAbXJvGtrn`)DvU8{rBXRFJ3IRs03u60yV*8@L{dtf&*y(zDwS~c z>eas!(cc}%c_dBKj~QdY7(=aAJA3^2@%AgPyaKKD-a!LEE|2OcVsdg4LI`xb-4AN5;rl+8mX^Ns>Z`9lf8oLfOioV1 zb=_@Gmfi48=9b}tSFT(c?KnviNGbR9Nz?Q#evbrl;JR+$dEWb^ zl>cltn_u^RAE!>8!u|K(4`U35m2iKcrIa{&^e9TD5;itAFc=JQ(ClHYMHEG-R;#%C z?z^$LxCkOTTqqR&+-kKxaq;3sIF5reXU?ElEFy}c{d-zUi696tGc$uUP2o5WeBZyN z;*$OEz3;gfoHP9EUl;2_hz|o>=`aWa6pO_h+SNUSR!U)Va&knGPN(y!jg5^*9LM3< zy|@5x7L@Cwh_upFu1x-_6qJ5)WNyw{3k_gYX+6)&c)owo#u&J+i+nzhPNxGYrQ2*a ze;tP5f$0GNGL`7{dk^Gt?k_yg3nwNfpp-(PP=J&YVHhG!)0@W4j(J*238fUq#>Nmu zkt!C8-!;ZOCWK%(nAjyLr5PU|f2vxmcGuc%G#V@LJP+5dU4zydjImJwX3Az?pos{L zMgx^f1=d=9=+L2W-E+@9KN#x9o6+Nj>_J47rm10!wURi&?|**|ob$2y`S}H{b<8;DahPAe{Q(L+tXkd$H-EJ4nW)qc41;!ZU^ZB;c`et~y1)b&PIF9k!Yp-P;W))(P&^|VgmVm9&sG+x%b$vA9frE?RFc+7@RzL68(Pv z15p%#b3T$|#bWWFnF!bs*KP}E0T~1V+U+(Pjm9mdydVf5r9`{kMjXeCG4`&EyF>(I z%=|@6R2k-L(d~9W-R*YwTwJyrHz=jBy4pHBea9Wh=l$Pkt^16z z_Y=`)t+iPZ5TSr`N7s+j~FF7|TjGrlzJa7z~n1rSg%9iHWZ<#sm&fT!=tv zDs>P9PgQGGT)BMt0ufzs9OtWPntpZ|c&1XRJaGK@@#N)~Up|-!$a3^hFFrRvKmXib z^K_;}-UPih>gx4+V8l?ZRzXAv!|+2|YmAMJp(1oP2zBb@kbE=gy(gXkct? zY)6Oe=;^F!UteE`=XvmbAHMJ7Ry^(dK9-l4vADR1BuU^nPPI@d+?yoHx#i{M@4K#x zGiT1=^y$;sO+?>RxoEBN#v5-S2mV_08bhY$k!d>%@v$zrkibQp#Y zzyA8`sMTt?@4oxsx-OC=*}eA|HYzl%N>)mVTrM|S$aP)xdOd7xY~1qXG?&X|gGE?t z7a3y@7mGz4K71H6Gc#};2U|l_WT#<=Ytj(s2()Xhb%&}rt<`E+TU&ceDHTHe*9Pp_ zu{(}~Uatq&bul+L2W#zUUdR~3;lqa!hG9Aw3?PJXt+g&N48$M^R)i3nQc7s8PZbJ< z1=n>COohD~V=N0sXst)(W)T5FU_r5}~c<)?uyp5Wrei%?48 z{Q2|msM{^26pS&bR;z!?ZnusdJ2turfbaWQSXg+FG4|Ly)f!wOIPU-e002ovPDHLk 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 zcmWkx2{csuAHO3pGMXVVg&0pV3S+4-mQ1$BQl>&=3uPNmwvc2Q<;)=jO--z_xDSes_Y<9UrXTpm}irL z2_bxoMBMEB(9n>BE(d|@;(E4IriNDiN2}UeU%gs4@PwsmMt=fM(>{I=HKK!GTCQOk7?)Ti#{2^KAc0i+)~B6sh9|u~z1ICdM-=YUlFruAocxV63`}+Er3kwT%WDkW$r6D7JTfTtC0Yi`8u*v8(RV^mNxq5V#XN2^G zLnbs*Jdp0GB_n>QM~VpCs(vi?G+Rz1dfKARRoHX+bqgE^y*b*ywBh>V=xZ&Oj$Nfa z%irI>hA2ZBPV(y{djL&y4t@>COI13$y1EL94Z@#~H`UYt1CbLt{SjJGwOfn6sg~%2 z19~F`J~bqSC0g7}JQiDr^00!5S0XZXr zYDXpz*}#Q+YFDC%8$Bd&2s80#l%DxujLi|s@L0GIs^!h9sz+$3Bp!VQ{{qs=aPDj2 z@@3nNxmYp-;x84x@Zp$O-HiYu5mjK@$=!>q(n1NoyCv@I?3_u6*Azl^RTGKCcigQA zK%v0a4P+GTVyJ!reA&UuEmj2O)5unlO8J{g?fU##DtbD}_bD{qy*L(4>+#ZZbagea z7>W0NdIeuFv6s5KeH3tjD8M=yJ8PmfghM1mYDPx)>Am7FuMh$h!q;HEfIt6C&=kUd ze=@n!viLB@-`}e7yT7&YZ7UKYR9{Xq#S8|Am$ANgd6*dcd`zN99654CfmG~NsraaL zd$Pn1FM^)G|J+wukJCxYR0QS}41}v{C=%2VYg=1p-I$n6uL5+y%uea>lS^5vC~nIO z%B=!h&QB1Ow7I(OAFHp|oTwWt{jSYJp3A2J#y=FT9KvDR<{+@!7lYco(2j#XBcT6& zo_xDbgo%9Gzc5!(Q86tL)ROl%9QTZZzy-V@ln;U-n(JFoN|7=GOilLPQ?HW{#sW3f ze%yn)`Tb1Sk8~oajyJxI>=#5f(^dPSF%BBH#%o?0t$yQ!WF~rPMc!__vr0ToBdzh& zIlH4Gs7Cm$L}3ACk^)pfNkZfb-s6Up){_qzpGQacYbFDyuPW2F*IY-}bk)eA`g?7d zLnV}*a>SPuAR5=RW`{R^;rXU;`Bv>OZ27fC`cli-)YMcSR~|Zsfn8HQO6l{4iHVi; z`S6K;etz0H_`TyY=*0giJOXS@9`E_oFyfJ_8c3pDkL@gzFaNiQ0WvzU7 zjwwd+KyF0f(YhRH=YMmPH%yS^OalKg;>%Kge*S+CD802#P@YN)7xlwLQ6(yb%X-OkMI}!82$3PK71?t8FPK?H~zHdIH8b9P~+zSu? zk16x#%)iYc5zDpRAN?9u;h0#*#K5W6WLya_pojGQ_%7Z=V1e36Cq@(|IaLaw1VI~> zG}9)$CyW|7sP*mKLZqa z_~Y)Qm)D~{>eVhRCgLDPyPr<|*eKeQB}qV0{7L#sV!%1x;`(|pwQ6A_a*S>`_7$`) zYa5%lcd3qvdYqawqV4<-3pJ$$UI;;zygpstw;MO3*AzjfE@#8QUU+FGhw7)5l$2cj z5zA<*_r8HVaU%Ur7NP@eD!+a_AjSzDrg8K57&iO}`s5tdt*Ee&pN&wIm6I`>nNEg2 z?Jm;2D?kkaC2eX)!oYZin~ayZ|e1PnO7`C_9(Wm`0aV)0rn0`jIX)_n!x2Nco88>wZzQ#T9sV$ zZYDo4G(b|VF|lzvM&?PVkcZ^0`}bui1SDA;Yi;AWb7TGhM4LI&q7JG|)6g&t1DoaX zSDKrfPg$6on_~+O%X!Jt&xnrrg`Iue8!Jw3oQe`nNlDSY`$f>2Me==m55jWiO`iGI zXC*_9OG^`@;7=+&?rnm3vVTsrk!X7Wo1V6s4w*98S5|jz{VTYcYON$;tVBvxrRlrW zzyQzGJww#j>tfLgv3eZPWXHzF+IM$b)vLdGJt&Sn`(tc;+(a>}{m5kl2(CwOaKotW zwHkmVzpdXBG6}-u#t8d9RsQ_>Gv?em@=bMNrlU1ZNJRIMB9a`}&~Vl<_h8>E1(I&F zgE1-7T)%)1iG8c3R*>#%Uq-=I+TU0BludZ#r6n@B@r6)BSsxQk9?R zJxjNfh2gK$K*;iZA-e=0B;{LtXHO4QN=gb8MRxDkWYF|)|2W1RQ$i_~Q}7bF7+|KG zd%a;7^S-{2GpP=f4*c-p!_4POXK$7chpWohW)`k&4dBc7^}fwo(5-pN)4#enR`ctL z`R4;;4gqZczHn*Qjx(z3{Fcy;aBr3xOWoBudPt{IG`u4akIn;r0qdm9;I*GbF891H z$89Lf(^&77_k^k9kG25y6ukEw4oME7hFn7LS)k|LzSAd{SERMIwI5P#U)0j+WIXMa zMz=ATKq%iOcFsh-dp4*0pTSzwKzfT=vJHCvPKF$oY+pfhyEn##fCb8!p?uU=DKDSofrM|KlxnX?Obu-&gLeL_{KMV520P!%f?T-m44yj6DL2u)EUv<# znrTMArb?Tx)0HgVyQZImf4gDOx-o!#c}-O_XP#PQqx=}8UnFazRoaGST5<(I31%3q zTT&xrpAjFNf=9(#Sy@Ff!NPe}Qa?|FVjS`*2A8Lz!ES(r=$1(C8nLg~+I;{15Yn@U zv!VN7M`wLI`sRkd<>4&?xaZ`0o1hb&8w1~w9%Oo4(#H0^`VtOqyo~+peXsbaEK2|& zCnag>=#Z|dhP@PgE*-R+5t#bePO`*b`<~d91Eby(!YA9Vi{UJtPf1<9dQ~L{U%TPams zvZ!?oSZ$H^Oo3q2udcb6p}MrUqg_wUz@>&zO0Z7DJu*Td*BiXsMd*)I1~`@Gbi*E; zs0gifjBazSepoUYShTb{Yry^^@s+8B~@KX}kC% z5X^9COo6cJZ5S=B_2H2U2e%PAm{(XvG2+@qm{19a1~)eEv!YUtg8r{DPH!t_zl)XY zHqkGLQG<0>ukZzDI_a}9rWWFObi6_=LIC8dTYf>ogUWlgx7wpDf&v3q4+6%#moZ<5 z!h-f)R3u#s^YiZ?Y+`?(I-1mQ{28I#0R(41=9BT*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 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 zcmWkycQ{*Z6pvMDsivr=R>i2OU1}?WR)Zq;Zltt!t=fC6C)@vXT==xI)H570+`~L!80sdvmR49Sl z)z=!P9v~1M)BhI*C@TjF0^Q<%s-|M#le1;%oM2(>8#L5H3i_U#nLB+lXIb50<(eZ? zP&~pr!p(;(UR-)YqmJXEE(^e3ON-++F#HhrYT-%6wOg@=#;WlJ+|0W1W-4hctXyT+ zuReWkz_ujQNHE-TAII@gQ<{~R<7S{Ex;BLw>lSpPuC@1Q4y=& zJhieS3)3P6;J7~uJIB*?FYiuBZb_$pTQ09u^#9Bdfd&)2w&HJ#2K_j?@a?@KtjVly ztXQ7TQ;dx%di9(QL*KbGY~Akj_kGpo3>wO?J?}2&EXnFEojrKukTuygK=t|Ew`K2e zE*~>6dbZfVnXka!?5zgx_L0KQb&w3GqKN|twIgXs;oYEJJ`KcslcGpIcc{97xFxNw zhh5tlL?Kf>E^m8ycvzPiRfx(a?S0~4j)c?*(WN1647eRC5gZwkL~U>2u?p1y%OBjBIt~>Ip-iadxSCi5XPCoA!*Kt$c#ybo)R^xx8J-gp}AD``$1woO79o+ISJT>~fpIs%I$)c3jCIz~^cC1VX*$ zDOX0=gdmiWSMg%X;85%Xx&msh&l3@&R`Rz;_Be~NZabVT7(O%bO{Ie%H(MoT0riTB z?%mx7MesO(E|}WEB7I079Oq33W4F+*e+-5t)OMwzf>6%v2YJ1SdcR6wn{tD&owJOL zjF6o(44?Ty8C$4*R&&6~10YN%qLHG^oDSIcVF)c420;`z=2a&VBrB)7v^#F$ZG_b5 zV7lc7V$#ylT;Q7a_5`haRDEuTiLXEz`T2d{ztgk#-I`JFT|F<~-4eHA-aT@ftN z3yFfskW}XhRk17x<#9>&M*+|T@#d;{c--U80!Oq-(H+cJ(w>Q>r89fmk26>c zK3_i>(D9PvkVy&`qc60d`FbS#Kt2NElNXJ|G2RHEy_w^|97nUf=lf&R%O{Vv&n<}( z?s9X;rYuw!{Fh_kQ^DqGmG-Z%C91wGz}xdkTNQ%d!mo!?GQV=A4=?;E*Vnk03VJP> zqOt7VYA3)~jCIswehA0eBVu-uo*)iTHr}f5tC~Ji56%q4YC+(LDS+d|=rQxOsz?E? zaYWnC-tKP7Ny4OF#l+iZrOA58Zw*z+katqrdu=GBHu4)t4djH-PzL*UV0TZ@v+XG{ zMvXfog{olu>*tcv+7b(Am(`-e!X3hKMVWInNp!4LYWC9Z(bOQb%GISv#NQnN9UZbt z!agub{}~YySdkPv4#8m`A~ar_So;|(&bZ~6Xo5i6;~EWS7s~HPkJ*M~i8~Z1q@Z(NZ&@!59>!s@->dg9y4**19GqhkA+y_~(mj^vbB zqbd|o8IC>Bcqrx&?mVXefgpycx|WJIsfTDNLoZFHnWL#~)GD6%+RX_;}-fqY1A8_GuFDjo>toxNS^U-?xw>yIXxf#A^&Ckz|UXuBhQ%z|yA*pRYb~-ty z1y+GZ0UfWix_2@^xZ(~Dd%lbeN%8k&RtI3Sw$f-7e-!wlZn}Cl@aU(teG?Ir2)%WO z;816sBdyK!aww8mE9Si(Yu@SBx68kLZP11}&ts&cP=)p1FnJBr8GGhQJD zj;%NTTq+ftr`FjeCp!0=pz1eeNHU&ly0fb_Q)yZs zeoyxIUvnZxp`o?ixGPz1*$lT9cOrt!E2@AFH>fad-yhS|;AIro!=jEmgPWf`D8+u_ z0(Y{ikKUz1}Xm2k`leu|y7?k)6u!iSDrOP_1Zo59irR5YMnR%3iu6}gfNY$n=y z%+22SCD(hdeBY5dzQ>YJ>W#J}j5-ZT(^|ztkI$gT(fn@myG>2;Db;lLBChT4QfY94 z!TpK(UKpq@#{*zS?_61r)%L1QV8>`Qw0UnxenEzzq@+YlT)eh|eXS}YxX+ED{f`b< zrD?q9#}DRhnIKw9r5Je%jMLy=JO9}F{uB3>**iA9n82--W+n!CO?3g-9QPJfeDIVGO7Fs=8)p?G0<(KcD%xiv-rX--{tQ=_ zxPY1-=VxS0RsDVl5Y69k?9Eavg7R?_ama(Sk`Y-bF{#5k>Sg&@FT$we7M+fHt*9E6 za^3X53;5_IWg3vDrFEmwW`6eIYF{KIc2lyJf6ee+kZ^9M$@0SRip9;V6Lq!~6Je*A z1ZTmu9V7gm*jL))x)l?;LM&)gqb~Yy+T-U=_fi)+ce;`xsJV%WEk;zo!-LH8g%qAA zmntv)2qV{v-w7z3&DJ%)zNEB!`H}*ZM+)^w+RSYGxxbM@t2!k6VZwo3v78PZR$wK> zqV!N&`p7$JN$apZqE^FCb#0V7M3Vk}TXS=B(WYER@7rfMw~c)Pg}r@=0@j}8C#m?<;rxG@bQA!Bes0PF#oPeH zSirC(?CuAICrAx>q6!kcaNe=+33g??IXOA`_S=gqIevy2kG4OM(XmF9f%nt8fo}bK zyp+t7q7j0MgU8u{VPqA7!7YQ|8dcwa`B8t4J|)iUE1Dr=)wn~=Dx=(6fEM!NQgyRk zRT##fpDUosvD_P+0=C;`q(eC^!-n^biD8x%-YDe9s;X|uK?-l34jXCwM4MIig{&$9 zJhLSQ`ZZOHaa#=W2nz7>^6Gm>pn4XcD8H$F&(+7U# z^d*slvXTJk9$5KPmw!QTUTUZ*pkq-1={S-njj!*o=aq*p$?(jNo0J$WySMltGo6Y5 zi!_IZnNB)#YZboH831!o_zjKmdL{zw4yvEuu_3FJQ#=b+}x+p+kcT&1{FCE4;-c1{Yh+SK*6me*baD-}4b2dZDPN~7*7hz$t(Doi!@ z2M-emHO20U0undY*l2Q)ckz34!^F^CEG19*L0SYF+Lvb5r0*Z`w#Yt<+&Dpf$VgRv z3vvER=l-SeKzE^P^oX^fmWIW=T?G2L6&^Ph@z$+U9@YdV6Kvc0!}A!zew_mqUSnq2 z11Nbg%)9){87o)6#eS*~E7m&Yo3a*uP-H;o>O(%(oC4 zwm%O0LIod5_}Xo+jV9;HxGhFAF);yvphh_0d0*cHJu8PS{#1cD(Pz^`GuND?=m=g* z@gPvIiG!Ql-|(uK9U;dxsXoh4x&w|T1Lueb7tAii-O(6F(wu~X7CAWMR=mnFFtVuSwirY}*jVf5JO zLE1;{I{Td9zXAH-zw+3z2uRpb?^}Kx_Q+W=9YRU)0_2jCUb(?#yaxkbtfsy`!mchB zJvWmgcxh~81YioSo$XBoJF!v8+4P@nnIYoYQ6iNz_wW5|Dj2U5@zFyGi9Pj!Pm_it zqkFlQmV`okGbwOb_v9}m4JP*)iS8uevaUZLFv1^vSER#IVj1udK|#Tc?CkhWSTPoi z|Fi$&#}8Uf+!#r5Snx5b(}x#DK-`9q^e`nSmZ0rR4+X~qD% zvcxB!bdvpJ)hO3JSvz-@c2@>oTY7WVdGbu95o%jnV%=LlPOecyNNcS6`ug*(O~kYN z_f7wa^b3?=D1qbLIA}o+`E8NN%(Z+YfbW4`L%kpM2`(MK*&k76N`K)y#?}M$BfFxX zq*Z|0tz4fxX~;jxRPUcv>RJIp{jpSbVzO;Qp;h%=H%ss5L7IiSuhY}h=g$XSgFIyO zhgHF$Mir5nnRnU`DqRyhD$z9+*KQmBop)$(fs-1ob@d89BpT6dD7xSFyHV2>AM{x) zfvm~P=r+B*yv)1Zw$phbpk`b(3>0xmX|+Jq7F#j4#+l90(NQM&^8EAecSYkZ-Q+(S zX# zK6<m=1o8TG-(E{wyRfU*r>nFpvji}xfbOlW zucsz9(q0iC^5sh(;6Nv!Jv{cx)6&w81pI%uiU2`B0>c1#nlfnjB+rN++lg%>>fTis0|ESY#bamK47lG0QW-&G6U-4r>3SR+LcuT@T^kVG$i&X zN$dc8wwYPEI02SD=g&a8D8|iAT!feXcGPtf(7!G}-%#rzt%0G)FidfM7jVJ7?Tim zk;}a_yPxLQNN%Snk|lLFZjEPZt!gGdUcv|3byJ48G@U(0uCv`hW;t|0)b+?&$dAGP zfLnU!Oe~cp$rIP+X60YHxNPs0*yqj5?I!t~#+cvgm`MtC&hf}7@eTU_QLI|`w1W+; zpC@+;2u59QuwlTgzOzAwPm9iQ$%vFa=TFhdu{1I9(P^=GS(F=89T5-dSe#woVcc{oe274QEqVBymMyFj5vSOL=3!6 zIzNo&q4);f1oZM6h<2DP7P(e@CpQ%+Lo`?Mh Dw?mVS literal 1348 zcmV-K1-tr*P)l>Z>txJIr0QU* zQB8TVEoAJ8-A#=-qu~Yk`xlO1!CU!!{#q@OF)=n3V;Ju!HITYQ;GiYOoUiz@Bc)Pl zskHsy!I&7EN`O!xj!C`JHUdX>O&s!8Es7I@v8hbbyV;Pmi=d|R$e5`pZE6jwyXgYm zlQt3jV4YyKK-56mL~sH*fu_I-lmaJE3ak^Ty}f-oO9ry1r)N!9SJz5uqr*CZxcI|j z?-e)M|0jTrMwUYG*+Ny{uBH%$Gh8IJX{tN7Vqb z=SpJnS(kx8uxWrJCt&|53!<%uXw!!n^)bFQ!se4dDbSsL0FLdsT&^u{JTfX!b93`z zIC@<)V4P@koB>OJAjAcnn~1Uh1`fvU_ZSt3DGk1m4w)vaZXC?WAuahVgss6d3;S1G~%rXOq_7$pKe z+u7N>)84X_K5Fl+NvJz-c z0HPG(_cI)CAhXxX^Kr0BAY{yLPU&ggN{3Yf%>+)M6j&z^d~RU<#n}vt=pEozX`{nB zfmrrnEE-efJ-%boMu&9*ah(+UkH4QIhDK?lBNKttVzJ?0h)R6}DLviD zn{n=O=s(v&(ncb0S|Hf9aNU99yDW&d-hzGH`jEbGnrGMYCo|{?jsqBn`oTjOm)6N^ zj0!|2l{k6>#)-hc^X`uWr?&Wj0(iM2#y~1v?oF`;19I-l;f53hY;2L2WCz+aR{RV5!}*bb<4?- zXQunuj#UEXa=9(=xiPI<>99(mnZOB@0_y}~C69Ga2mH@tpf1GS9@1f*KrBN-{~^$4 zh;>lf=&(v4FSreLUK8j61llO=bYvn>OH0cNrgj$O23WQA4QDa<{WA1_PE_-%_IV{5 zF-o*|nc@rP>D>XYz~wbY1ww#xInx|4oB8w0Bwc+9+QMPtsJP8II-_`@p&Nl z^Pb``%i_9~j=X7s<`S4a(1*K<@3{V{)~$3{C6L;a;wzU^b{ng~DuLMb)XbXLt#nu= z&`jV2N`Vt71x}z8IDt~|R|2V`&T4_EfwYNGa4+>Hztrow*TR!u3um3}o=2uxO~cc9N0>w=lbI_kC6$Va7Nz&9x;VX$ZD?rVzgUr-6B<`M zoT5N8hZ!Pg zni-=r(RU}Uf(I1x?F)rcx@wcUW~Oi7tQ3$9);Xq>iG6>3hF+<0pMgcL!K1w zPOZvXIjPT*prLi)&$)%KG#2s;T&!3h__cr>d#o>qBg_P5QP14b6cO&I z@E|etw?uN2-&kBe4Oi#{Kk7cG^A@R;Uf*e(wf6GDoh$CKVejjU$dE`zegv7ZN||bl z08^5alcycwP^6b$tx{q>{brzy=R}qbYUj;fk9_Zt@cXm3Vz}co&M7nmyO*VbS zBmtF{Pxg>U#UFNyyWFe(!`|1P zX8&=BNJ>e~>2|p~QG}^;hQ0G+-g_{mzLjH-zB)NU&0zSju!E_>P=u+W+H4-+(4wC0 z*!wkPzj>0SaX%tF{CFpWL{V`80$XIfokiYhyq$$j2DZ4n^*sk1JQQNUt(g?DH0ti7 zrePs7m=6I11@>Jhd+SmWW{R1>xsmWO*Owoc+_c~V;cZjXmN4gnLVLgz5?^caIvFD9 z5fdXj&tUAaV_w!kMF7AiE6%@R+|4_EJSVl1;&tcd0Gl5O_HU}MP9t-hgsW@PCAa>K z;T4zy6<2dKF`fqE`d&=^7__#s3Un&iU+L58EK+@26HV3Z8F^#4gaX0Y2MRXyBAjP+ zX&J=5GcI=Xo{awVJ9M=m$y=aV1CzJ*z*~pCxwo@fi_ITK({8m)vS^$eIjOD?AXl3& zy`FLTFw4tDJ=pnUjw{zWx zH>ii*cW=GtF&DHR&e3v@yD+vcTqy7w39@Grtoa|yD<#IbT11$uCHZ4^E~5}9TV_U! zSJ^T0z;3D2psR)fgWLbxkL63Ue+;9AVG2XvT-HLAZHx--RKU=E&fY}QV(HCujgu@6 zQEUa2W5k%J*}s+4Av+taHYR=>;!oxoRSc(|-tUhnVBez7Z1xo%HDK%|W>~MI zn~1Bs1;+m>NV6)b1NY-T69rvK{>gdcUig?!>*rwnrz!3uDqK|4 z> zxnihwG%?#B<2uS%rHx&b15C3@r>LIds(SMC*|tZyJzx^iHdTm{Yl~Q#ri1LK1<@p2 zRnyg+r29ohMH0ZDGB#DZ>=;fetK-Lyz#yZ146WWXo$^@Il><+sa-QvO_4oJFH-$Co z-e3JV>uQfetWL1PyHp6;e9Mu$Ubajv^+DED+Z7-G=XuJ2lDJygN!s?OYS)(Oi%#qO zRtzg^>zvbyWNB19g+h@8&-sC1=Nu)0-`pzH(+p;iKd>ZsI_5+0_1%T{m1bUD(kS3T zPs41>2=gNa8G89~N%kK#ize-1i=&5R5Pr~651yEf@mm^v&iX;qvI`3fkO4#9ctrq@ z%{tI}?Z=h7#W4;nx7tq5i=ht=9y|vicgmI$#Ui(U&J5V8@M7}7d%ULnWpLFpun*VJ znU|Z(W0Un)TiaxCVn@Ivrs$Vj8N_a{(IW;7zfZM|wMDJsxD}L8H~>?)GCx1xo%Kc; zkH-V#Ip}@Q`U(~^+4K&yPIW>t+fj==H$k*5o9Ft-CIW%*xN(CAZYka@exh)j^X_7z|tUP$V9| zW*ZT)dHuuZe>eM=R0Z{nu47sPt`x!l_XhAEI~pOCWwc@` zU#ZG+T62SxS~(39?>tfEqf0j{s$1^IRueI4xKa;Rqcd6w>eohta1%@iY#f}Rro%^Y zi`}OJMzIz)S>a<7&Ms&r*7xt%Zdkq866YzW5wZi60}wQGfkvs_zQ>)d52N*ob1OiX_v2=>EU#yHzq=Soz;i zC%zmUO!9M)F_^jOgfSm3sqH z$MNau=@wk1K~Rb`KY`s$e8DTi$J5z|HtpGa9NoB1u;%u7G^Vg? zCOAl0LMp9iu1jy*+xuRovhS<8WxMT|m#HqZkauc76-lHYdHMOqflqA9+KH=&D(}L5 zVP?ICIen5xkjUp(s|RCeU*PE0WSK>bJvCga89ANYmz#TT0_7*i}9)J7?>g;0X 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 From 9a6f88b81baf82718ea1c008d25656681c7a752b Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 29 Mar 2023 15:14:52 -0400 Subject: [PATCH 037/145] Turning LocalCache Listeners into an Additive Feed type. --- .../amethyst/model/LocalCache.kt | 61 +++++++++---------- .../amethyst/ui/components/BundledUpdate.kt | 36 +++++++++++ .../amethyst/ui/dal/ChannelFeedFilter.kt | 12 +++- .../amethyst/ui/dal/ChatroomFeedFilter.kt | 21 ++++++- .../amethyst/ui/dal/FeedFilter.kt | 23 ++++++- .../amethyst/ui/dal/GlobalFeedFilter.kt | 42 ++++++------- .../amethyst/ui/dal/HashtagFeedFilter.kt | 24 +++++--- .../ui/dal/HomeConversationsFeedFilter.kt | 20 ++++-- .../ui/dal/HomeNewThreadFeedFilter.kt | 34 ++++++----- .../amethyst/ui/dal/NotificationFeedFilter.kt | 19 ++++-- .../amethyst/ui/screen/CardFeedViewModel.kt | 7 +-- .../amethyst/ui/screen/FeedViewModel.kt | 34 +++++++++-- .../amethyst/ui/screen/LnZapFeedViewModel.kt | 7 +-- .../amethyst/ui/screen/UserFeedViewModel.kt | 8 +-- 14 files changed, 242 insertions(+), 106 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 da8686412b..e0831547ad 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -27,7 +27,7 @@ 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.relays.Relay -import com.vitorpamplona.amethyst.ui.components.BundledUpdate +import com.vitorpamplona.amethyst.ui.components.BundledInsert import fr.acinq.secp256k1.Hex import kotlinx.coroutines.* import nostr.postr.toNpub @@ -208,7 +208,7 @@ object LocalCache { it.addReply(note) } - refreshObservers() + refreshObservers(note) } fun consume(event: LongTextNoteEvent, relay: Relay?) { @@ -237,7 +237,7 @@ object LocalCache { author.addNote(note) - refreshObservers() + refreshObservers(note) } } @@ -251,7 +251,7 @@ object LocalCache { if (event.createdAt > (note.createdAt() ?: 0)) { note.loadEvent(event, author, emptyList()) - refreshObservers() + refreshObservers(note) } } @@ -269,8 +269,6 @@ object LocalCache { note.loadEvent(event, author, replyTo) author.updateAcceptedBadges(note) - - refreshObservers() } } @@ -292,7 +290,7 @@ object LocalCache { it.addReply(note) } - refreshObservers() + refreshObservers(note) } @Suppress("UNUSED_PARAMETER") @@ -338,7 +336,7 @@ object LocalCache { recipient.addMessage(author, note) } - refreshObservers() + refreshObservers(note) } fun consume(event: DeletionEvent) { @@ -386,7 +384,7 @@ object LocalCache { } if (deletedAtLeastOne) { - live.invalidateData() + // refreshObservers() } } @@ -412,7 +410,7 @@ object LocalCache { it.addBoost(note) } - refreshObservers() + refreshObservers(note) } fun consume(event: ReactionEvent) { @@ -450,6 +448,8 @@ object LocalCache { it.addReport(note) } } + + refreshObservers(note) } fun consume(event: ReportEvent, relay: Relay?) { @@ -480,6 +480,8 @@ object LocalCache { repliesTo.forEach { it.addReport(note) } + + refreshObservers(note) } fun consume(event: ChannelCreateEvent) { @@ -496,7 +498,7 @@ object LocalCache { oldChannel.addNote(note) note.loadEvent(event, author, emptyList()) - refreshObservers() + refreshObservers(note) } } @@ -516,7 +518,7 @@ object LocalCache { oldChannel.addNote(note) note.loadEvent(event, author, emptyList()) - refreshObservers() + refreshObservers(note) } } else { // Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} ${formattedDateTime(event.createdAt)} > ${formattedDateTime(oldUser.updatedAt)}") @@ -563,7 +565,7 @@ object LocalCache { it.addReply(note) } - refreshObservers() + refreshObservers(note) } @Suppress("UNUSED_PARAMETER") @@ -606,6 +608,8 @@ object LocalCache { mentions.forEach { it.addZap(zapRequest, note) } + + refreshObservers(note) } fun consume(event: LnZapRequestEvent) { @@ -629,6 +633,8 @@ object LocalCache { mentions.forEach { it.addZap(note, null) } + + refreshObservers(note) } fun findUsersStartingWith(username: String): List { @@ -734,30 +740,23 @@ object LocalCache { } // Observers line up here. - val live: LocalCacheLiveData = LocalCacheLiveData(this) + val live: LocalCacheLiveData = LocalCacheLiveData() - private fun refreshObservers() { - live.invalidateData() + private fun refreshObservers(newNote: Note) { + live.invalidateData(newNote) } } -class LocalCacheLiveData(val cache: LocalCache) : - LiveData(LocalCacheState(cache)) { +class LocalCacheLiveData : LiveData>() { // Refreshes observers in batches. - private val bundler = BundledUpdate(300, Dispatchers.Main) { - if (hasActiveObservers()) { - refresh() + private val bundler = BundledInsert(300, Dispatchers.Main) + + fun invalidateData(newNote: Note) { + bundler.invalidateList(newNote) { bundledNewNotes -> + if (hasActiveObservers()) { + postValue(bundledNewNotes) + } } } - - fun invalidateData() { - bundler.invalidate() - } - - private fun refresh() { - postValue(LocalCacheState(cache)) - } } - -class LocalCacheState(val cache: LocalCache) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/BundledUpdate.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/BundledUpdate.kt index d2172e560d..3d118e08cf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/BundledUpdate.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/BundledUpdate.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference /** * This class is designed to have a waiting time between two calls of invalidate @@ -44,3 +45,38 @@ class BundledUpdate( } } } + +/** + * This class is designed to have a waiting time between two calls of invalidate + */ +class BundledInsert( + val delay: Long, + val dispatcher: CoroutineDispatcher = Dispatchers.Default +) { + private var onlyOneInBlock = AtomicBoolean() + private var atomicSet = AtomicReference>(setOf()) + + fun invalidateList(newObject: T, onUpdate: (Set) -> Unit) { + // atomicSet.updateAndGet() { + // it + newObject + // } + + if (onlyOneInBlock.getAndSet(true)) { + return + } + + val scope = CoroutineScope(Job() + dispatcher) + scope.launch { + try { + // onUpdate(atomicSet.getAndSet(emptySet())) + onUpdate(emptySet()) + delay(delay) + // onUpdate(atomicSet.getAndSet(emptySet())) + } finally { + withContext(NonCancellable) { + onlyOneInBlock.set(false) + } + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChannelFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChannelFeedFilter.kt index 6e19217dd7..14e4b09554 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChannelFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChannelFeedFilter.kt @@ -5,7 +5,7 @@ import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note -object ChannelFeedFilter : FeedFilter() { +object ChannelFeedFilter : AdditiveFeedFilter() { lateinit var account: Account lateinit var channel: Channel @@ -22,4 +22,14 @@ object ChannelFeedFilter : FeedFilter() { .sortedBy { it.createdAt() } .reversed() } + + override fun applyFilter(collection: Set): List { + return collection + .filter { it.idHex in channel.notes.keys } + .filter { account.isAcceptable(it) } + } + + override fun sort(collection: List): List { + return collection.sortedBy { it.createdAt() }.reversed() + } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt index 9d77e1ac41..d885ce7e4a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt @@ -5,7 +5,7 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -object ChatroomFeedFilter : FeedFilter() { +object ChatroomFeedFilter : AdditiveFeedFilter() { var account: Account? = null var withUser: User? = null @@ -30,4 +30,23 @@ object ChatroomFeedFilter : FeedFilter() { .sortedBy { it.createdAt() } .reversed() } + + override fun applyFilter(collection: Set): List { + val myAccount = account + val myUser = withUser + + if (myAccount == null || myUser == null) return emptyList() + + val messages = myAccount + .userProfile() + .privateChatrooms[myUser] ?: return emptyList() + + return collection + .filter { it in messages.roomMessages } + .filter { account?.isAcceptable(it) == true } + } + + override fun sort(collection: List): List { + return collection.sortedBy { it.createdAt() }.reversed() + } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt index e5f78f4e26..d4395f8da2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt @@ -4,7 +4,7 @@ import android.util.Log import kotlin.time.ExperimentalTime import kotlin.time.measureTimedValue -abstract class FeedFilter() { +abstract class FeedFilter { @OptIn(ExperimentalTime::class) fun loadTop(): List { val (feed, elapsed) = measureTimedValue { @@ -17,3 +17,24 @@ abstract class FeedFilter() { abstract fun feed(): List } + +abstract class AdditiveFeedFilter : FeedFilter() { + abstract fun applyFilter(collection: Set): List + abstract fun sort(collection: List): List + + @OptIn(ExperimentalTime::class) + fun updateListWith(oldList: List, newItems: Set): List { + val (feed, elapsed) = measureTimedValue { + val newItemsToBeAdded = applyFilter(newItems) + if (newItemsToBeAdded.isNotEmpty()) { + val newList = oldList + newItemsToBeAdded + sort(newList).take(1000) + } else { + oldList + } + } + + Log.d("Time", "${this.javaClass.simpleName} Feed in $elapsed with ${feed.size} objects") + return feed + } +} 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 ffed429cc3..9122dd8f62 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 @@ -7,18 +7,28 @@ import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent -object GlobalFeedFilter : FeedFilter() { +object GlobalFeedFilter : AdditiveFeedFilter() { lateinit var account: Account override fun feed(): List { + val notes = applyFilter(LocalCache.notes.values) + val longFormNotes = applyFilter(LocalCache.addressables.values) + + return sort(notes + longFormNotes) + } + + override fun applyFilter(collection: Set): List { + return applyFilter(collection) + } + + private fun applyFilter(collection: Collection): List { val followChannels = account.followingChannels() val followUsers = account.followingKeySet() - val notes = LocalCache.notes.values + return collection .asSequence() .filter { - (it.event is TextNoteEvent || it.event is LongTextNoteEvent || it.event is ChannelMessageEvent) && - it.replyTo.isNullOrEmpty() + (it.event is TextNoteEvent || it.event is LongTextNoteEvent || it.event is ChannelMessageEvent) && it.replyTo.isNullOrEmpty() } .filter { // does not show events already in the public chat list @@ -32,27 +42,9 @@ object GlobalFeedFilter : FeedFilter() { it.createdAt()!! <= System.currentTimeMillis() / 1000 } .toList() + } - val longFormNotes = LocalCache.addressables.values - .asSequence() - .filter { - (it.event is LongTextNoteEvent) && it.replyTo.isNullOrEmpty() - } - .filter { - // does not show events already in the public chat list - (it.channel() == null || it.channel() !in followChannels) && - // does not show people the user already follows - (it.author?.pubkeyHex !in followUsers) - } - .filter { account.isAcceptable(it) } - .filter { - // Do not show notes with the creation time exceeding the current time, as they will always stay at the top of the global feed, which is cheating. - it.createdAt()!! <= System.currentTimeMillis() / 1000 - } - .toList() - - return (notes + longFormNotes) - .sortedBy { it.createdAt() } - .reversed() + override fun sort(collection: List): List { + return collection.sortedBy { it.createdAt() }.reversed() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt index 844505ff3f..c33593ff40 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt @@ -8,14 +8,27 @@ import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.PrivateDmEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent -object HashtagFeedFilter : FeedFilter() { +object HashtagFeedFilter : AdditiveFeedFilter() { lateinit var account: Account var tag: String? = null + fun loadHashtag(account: Account, tag: String?) { + this.account = account + this.tag = tag + } + override fun feed(): List { + return sort(applyFilter(LocalCache.notes.values)) + } + + override fun applyFilter(collection: Set): List { + return applyFilter(collection) + } + + private fun applyFilter(collection: Collection): List { val myTag = tag ?: return emptyList() - return LocalCache.notes.values + return collection .asSequence() .filter { ( @@ -27,13 +40,10 @@ object HashtagFeedFilter : FeedFilter() { it.event?.isTaggedHash(myTag) == true } .filter { account.isAcceptable(it) } - .sortedBy { it.createdAt() } .toList() - .reversed() } - fun loadHashtag(account: Account, tag: String?) { - this.account = account - this.tag = tag + override fun sort(collection: List): List { + return collection.sortedBy { it.createdAt() }.reversed() } } 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 50385acfd7..3a2fda4704 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 @@ -5,15 +5,24 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.TextNoteEvent -object HomeConversationsFeedFilter : FeedFilter() { +object HomeConversationsFeedFilter : AdditiveFeedFilter() { lateinit var account: Account override fun feed(): List { + return sort(applyFilter(LocalCache.notes.values)) + } + + override fun applyFilter(collection: Set): List { + return applyFilter(collection) + } + + private fun applyFilter(collection: Collection): List { val user = account.userProfile() val followingKeySet = user.cachedFollowingKeySet() val followingTagSet = user.cachedFollowingTagSet() - return LocalCache.notes.values + return collection + .asSequence() .filter { (it.event is TextNoteEvent) && (it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false)) && @@ -21,7 +30,10 @@ object HomeConversationsFeedFilter : FeedFilter() { it.author?.let { !account.isHidden(it) } ?: true && !it.isNewThread() } - .sortedBy { it.createdAt() } - .reversed() + .toList() + } + + override fun sort(collection: List): List { + return collection.sortedBy { it.createdAt() }.reversed() } } 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 ca88aaa96f..5ae61e0170 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 @@ -7,34 +7,38 @@ import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent -object HomeNewThreadFeedFilter : FeedFilter() { +object HomeNewThreadFeedFilter : AdditiveFeedFilter() { lateinit var account: Account override fun feed(): List { + val notes = applyFilter(LocalCache.notes.values) + val longFormNotes = applyFilter(LocalCache.addressables.values) + + return sort(notes + longFormNotes) + } + + override fun applyFilter(collection: Set): List { + return applyFilter(collection) + } + + private fun applyFilter(collection: Collection): List { val user = account.userProfile() val followingKeySet = user.cachedFollowingKeySet() val followingTagSet = user.cachedFollowingTagSet() - val notes = LocalCache.notes.values + return collection + .asSequence() .filter { it -> - (it.event is TextNoteEvent || it.event is RepostEvent) && + (it.event is TextNoteEvent || it.event is RepostEvent || it.event is LongTextNoteEvent) && (it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false)) && // && account.isAcceptable(it) // This filter follows only. No need to check if acceptable it.author?.let { !account.isHidden(it) } ?: true && it.isNewThread() } + .toList() + } - val longFormNotes = LocalCache.addressables.values - .filter { it -> - (it.event is LongTextNoteEvent) && - (it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false)) && - // && account.isAcceptable(it) // This filter follows only. No need to check if acceptable - it.author?.let { !account.isHidden(it) } ?: true && - it.isNewThread() - } - - return (notes + longFormNotes) - .sortedBy { it.createdAt() } - .reversed() + override fun sort(collection: List): List { + return collection.sortedBy { it.createdAt() }.reversed() } } 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 39dcc920e6..aa9327bf07 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 @@ -5,12 +5,21 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.* -object NotificationFeedFilter : FeedFilter() { +object NotificationFeedFilter : AdditiveFeedFilter() { lateinit var account: Account override fun feed(): List { + return sort(applyFilter(LocalCache.notes.values)) + } + + override fun applyFilter(collection: Set): List { + return applyFilter(collection) + } + + private fun applyFilter(collection: Collection): List { val loggedInUser = account.userProfile() - return LocalCache.notes.values + + return collection .asSequence() .filter { it.event !is ChannelCreateEvent && @@ -36,8 +45,10 @@ object NotificationFeedFilter : FeedFilter() { it.replyTo?.lastOrNull()?.author == loggedInUser || loggedInUser in it.directlyCiteUsers() } - .sortedBy { it.createdAt() } .toList() - .reversed() + } + + override fun sort(collection: List): List { + return collection.sortedBy { it.createdAt() }.reversed() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt index 34337677e4..5debefce80 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt @@ -4,7 +4,6 @@ import android.util.Log import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.LocalCacheState import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent @@ -47,7 +46,7 @@ open class CardFeedViewModel(val dataSource: FeedFilter) : ViewModel() { val lastNotesCopy = lastNotes - val oldNotesState = feedContent.value + val oldNotesState = _feedContent.value if (lastNotesCopy != null && oldNotesState is CardFeedState.Loaded) { val newCards = convertToCard(notes.minus(lastNotesCopy)) if (newCards.isNotEmpty()) { @@ -125,7 +124,7 @@ open class CardFeedViewModel(val dataSource: FeedFilter) : ViewModel() { private fun updateFeed(notes: List) { val scope = CoroutineScope(Job() + Dispatchers.Main) scope.launch { - val currentState = feedContent.value + val currentState = _feedContent.value if (notes.isEmpty()) { _feedContent.update { CardFeedState.Empty } @@ -152,7 +151,7 @@ open class CardFeedViewModel(val dataSource: FeedFilter) : ViewModel() { bundler.invalidate() } - private val cacheListener: (LocalCacheState) -> Unit = { + private val cacheListener: (Set) -> Unit = { newNotes -> invalidateData() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index a6b2fcdb13..60daf10722 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -3,9 +3,10 @@ package com.vitorpamplona.amethyst.ui.screen import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.LocalCacheState import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.components.BundledInsert import com.vitorpamplona.amethyst.ui.components.BundledUpdate +import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.BookmarkPrivateFeedFilter import com.vitorpamplona.amethyst.ui.dal.BookmarkPublicFeedFilter import com.vitorpamplona.amethyst.ui.dal.ChannelFeedFilter @@ -65,7 +66,7 @@ abstract class FeedViewModel(val localFilter: FeedFilter) : ViewModel() { fun refreshSuspended() { val notes = newListFromDataSource() - val oldNotesState = feedContent.value + val oldNotesState = _feedContent.value if (oldNotesState is FeedState.Loaded) { // Using size as a proxy for has changed. if (notes != oldNotesState.feed.value) { @@ -79,7 +80,7 @@ abstract class FeedViewModel(val localFilter: FeedFilter) : ViewModel() { private fun updateFeed(notes: List) { val scope = CoroutineScope(Job() + Dispatchers.Main) scope.launch { - val currentState = feedContent.value + val currentState = _feedContent.value if (notes.isEmpty()) { _feedContent.update { FeedState.Empty } } else if (currentState is FeedState.Loaded) { @@ -91,18 +92,41 @@ abstract class FeedViewModel(val localFilter: FeedFilter) : ViewModel() { } } + fun refreshFromOldState(newItems: Set) { + val oldNotesState = _feedContent.value + if (localFilter is AdditiveFeedFilter && oldNotesState is FeedState.Loaded) { + val newList = localFilter.updateListWith(oldNotesState.feed.value, newItems.toSet()) + updateFeed(newList) + } else { + // Refresh Everything + refreshSuspended() + } + } + private val bundler = BundledUpdate(250, Dispatchers.IO) { // adds the time to perform the refresh into this delay // holding off new updates in case of heavy refresh routines. refreshSuspended() } + private val bundlerInsert = BundledInsert>(250, Dispatchers.IO) fun invalidateData() { bundler.invalidate() } - private val cacheListener: (LocalCacheState) -> Unit = { - invalidateData() + fun invalidateInsertData(newItems: Set) { + bundlerInsert.invalidateList(newItems) { + refreshFromOldState(it.flatten().toSet()) + } + } + + private val cacheListener: (Set) -> Unit = { newNotes -> + if (localFilter is AdditiveFeedFilter && _feedContent.value is FeedState.Loaded) { + invalidateInsertData(newNotes) + } else { + // Refresh Everything + invalidateData() + } } init { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedViewModel.kt index 9973afab81..8c6806de81 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedViewModel.kt @@ -3,7 +3,6 @@ package com.vitorpamplona.amethyst.ui.screen import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.LocalCacheState import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.BundledUpdate import com.vitorpamplona.amethyst.ui.dal.FeedFilter @@ -32,7 +31,7 @@ open class LnZapFeedViewModel(val dataSource: FeedFilter>) : Vi private fun refreshSuspended() { val notes = dataSource.loadTop() - val oldNotesState = feedContent.value + val oldNotesState = _feedContent.value if (oldNotesState is LnZapFeedState.Loaded) { // Using size as a proxy for has changed. if (notes != oldNotesState.feed.value) { @@ -46,7 +45,7 @@ open class LnZapFeedViewModel(val dataSource: FeedFilter>) : Vi private fun updateFeed(notes: List>) { val scope = CoroutineScope(Job() + Dispatchers.Main) scope.launch { - val currentState = feedContent.value + val currentState = _feedContent.value if (notes.isEmpty()) { _feedContent.update { LnZapFeedState.Empty } } else if (currentState is LnZapFeedState.Loaded) { @@ -68,7 +67,7 @@ open class LnZapFeedViewModel(val dataSource: FeedFilter>) : Vi bundler.invalidate() } - private val cacheListener: (LocalCacheState) -> Unit = { + private val cacheListener: (Set) -> Unit = { newNotes -> invalidateData() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt index 500320035a..aae7fd3581 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt @@ -3,7 +3,7 @@ package com.vitorpamplona.amethyst.ui.screen import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.LocalCacheState +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.components.BundledUpdate import com.vitorpamplona.amethyst.ui.dal.FeedFilter @@ -36,7 +36,7 @@ open class UserFeedViewModel(val dataSource: FeedFilter) : ViewModel() { private fun refreshSuspended() { val notes = dataSource.loadTop() - val oldNotesState = feedContent.value + val oldNotesState = _feedContent.value if (oldNotesState is UserFeedState.Loaded) { // Using size as a proxy for has changed. if (notes != oldNotesState.feed.value) { @@ -50,7 +50,7 @@ open class UserFeedViewModel(val dataSource: FeedFilter) : ViewModel() { private fun updateFeed(notes: List) { val scope = CoroutineScope(Job() + Dispatchers.Main) scope.launch { - val currentState = feedContent.value + val currentState = _feedContent.value if (notes.isEmpty()) { _feedContent.update { UserFeedState.Empty } } else if (currentState is UserFeedState.Loaded) { @@ -72,7 +72,7 @@ open class UserFeedViewModel(val dataSource: FeedFilter) : ViewModel() { bundler.invalidate() } - private val cacheListener: (LocalCacheState) -> Unit = { + private val cacheListener: (Set) -> Unit = { newNotes -> invalidateData() } From 24dc4ba7a55b2697a45b41d85897ca261ed4d860 Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Fri, 31 Mar 2023 13:33:42 +0900 Subject: [PATCH 038/145] remove hacky fun for getting poll_option from ZapEvent --- .../vitorpamplona/amethyst/model/Account.kt | 1694 ++++++++--------- .../amethyst/service/model/LnZapEvent.kt | 139 +- .../service/model/LnZapRequestEvent.kt | 215 ++- .../amethyst/ui/note/ZapCustomDialog.kt | 387 ++-- 4 files changed, 1205 insertions(+), 1230 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 ce1c4d9224..1d7d3b1de0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -1,855 +1,839 @@ -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.BookmarkListEvent -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.LnZapPaymentRequestEvent -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.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 com.vitorpamplona.amethyst.ui.components.BundledUpdate -import com.vitorpamplona.amethyst.ui.note.Nip47URI -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import com.vitorpamplona.amethyst.service.model.* -import com.vitorpamplona.amethyst.service.relays.* -import kotlinx.coroutines.* -import nostr.postr.Persona -import java.util.* -import java.util.Locale - -val DefaultChannels = setOf( - "25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb", // -> Anigma's Nostr - "42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5" // -> Amethyst's Group -) - -fun getLanguagesSpokenByUser(): Set { - val languageList = ConfigurationCompat.getLocales(Resources.getSystem().getConfiguration()) - val codedList = mutableSetOf() - for (i in 0 until languageList.size()) { - languageList.get(i)?.let { codedList.add(it.language) } - } - return codedList -} - -@OptIn(DelicateCoroutinesApi::class) -class Account( - val loggedIn: Persona, - var followingChannels: Set = DefaultChannels, - var hiddenUsers: Set = setOf(), - var localRelays: Set = Constants.defaultRelays.toSet(), - var dontTranslateFrom: Set = getLanguagesSpokenByUser(), - var languagePreferences: Map = mapOf(), - var translateTo: String = Locale.getDefault().language, - var zapAmountChoices: List = listOf(500L, 1000L, 5000L), - var zapPaymentRequest: Nip47URI? = null, - var hideDeleteRequestDialog: Boolean = false, - var hideBlockAlertDialog: Boolean = false, - var backupContactList: ContactListEvent? = null -) { - var transientHiddenUsers: Set = setOf() - - // Observers line up here. - val live: AccountLiveData = AccountLiveData(this) - val liveLanguages: AccountLiveData = AccountLiveData(this) - val saveable: AccountLiveData = AccountLiveData(this) - - fun userProfile(): User { - return LocalCache.getOrCreateUser(loggedIn.pubKey.toHexKey()) - } - - fun followingChannels(): List { - return followingChannels.map { LocalCache.getOrCreateChannel(it) } - } - - fun hiddenUsers(): List { - return (hiddenUsers + transientHiddenUsers).map { LocalCache.getOrCreateUser(it) } - } - - fun isWriteable(): Boolean { - return loggedIn.privKey != null - } - - fun sendNewRelayList(relays: Map) { - if (!isWriteable()) return - - val contactList = userProfile().latestContactList - val follows = contactList?.follows() ?: emptyList() - val followsTags = contactList?.unverifiedFollowTagSet() ?: emptyList() - - if (contactList != null && follows.isNotEmpty()) { - val event = ContactListEvent.create( - follows, - followsTags, - relays, - loggedIn.privKey!! - ) - - Client.send(event) - LocalCache.consume(event) - } else { - val event = ContactListEvent.create(listOf(), listOf(), relays, loggedIn.privKey!!) - - // Keep this local to avoid erasing a good contact list. - // Client.send(event) - LocalCache.consume(event) - } - } - - fun sendNewUserMetadata(toString: String, identities: List) { - if (!isWriteable()) return - - loggedIn.privKey?.let { - val event = MetadataEvent.create(toString, identities, loggedIn.privKey!!) - Client.send(event) - LocalCache.consume(event) - } - } - - fun reactionTo(note: Note): List { - return note.reactedBy(userProfile(), "+") - } - - fun hasBoosted(note: Note): Boolean { - return boostsTo(note).isNotEmpty() - } - - fun boostsTo(note: Note): List { - return note.boostedBy(userProfile()) - } - - fun hasReacted(note: Note): Boolean { - return note.hasReacted(userProfile(), "+") - } - - fun reactTo(note: Note) { - if (!isWriteable()) return - - if (hasReacted(note)) { - // has already liked this note - return - } - - note.event?.let { - val event = ReactionEvent.createLike(it, loggedIn.privKey!!) - Client.send(event) - LocalCache.consume(event) - } - } - - fun createZapRequestFor(note: Note, pollOption: Int?, message: String = ""): LnZapRequestEvent? { - if (!isWriteable()) return null - - note.event?.let { event -> - return LnZapRequestEvent.create( - event, - userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } - ?: localRelays.map { it.url }.toSet(), - loggedIn.privKey!!, - pollOption - ) - /*note.event?.let { - return LnZapRequestEvent.create(it, userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } ?: localRelays.map { it.url }.toSet(), loggedIn.privKey!!, message) - }*/ - - return null - } - - fun hasWalletConnectSetup(): Boolean { - return zapPaymentRequest != null - } - - fun sendZapPaymentRequestFor(lnInvoice: String) { - if (!isWriteable()) return - - zapPaymentRequest?.let { - val event = LnZapPaymentRequestEvent.create(lnInvoice, it.pubKeyHex, it.secret?.toByteArray() ?: loggedIn.privKey!!) - - Client.send(event, it.relayUri) - } - } - - fun createZapRequestFor(user: User): LnZapRequestEvent? { - return createZapRequestFor(user.pubkeyHex) - } - - fun createZapRequestFor(userPubKeyHex: String, message: String = ""): LnZapRequestEvent? { - if (!isWriteable()) return null - - return LnZapRequestEvent.create(userPubKeyHex, userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } ?: localRelays.map { it.url }.toSet(), loggedIn.privKey!!, message) - } - - fun report(note: Note, type: ReportEvent.ReportType, content: String = "") { - if (!isWriteable()) return - - if (note.hasReacted(userProfile(), "⚠️")) { - // has already liked this note - return - } - - note.event?.let { - val event = ReactionEvent.createWarning(it, loggedIn.privKey!!) - Client.send(event) - LocalCache.consume(event) - } - - note.event?.let { - val event = ReportEvent.create(it, type, loggedIn.privKey!!, content = content) - Client.send(event) - LocalCache.consume(event, null) - } - } - - fun report(user: User, type: ReportEvent.ReportType) { - if (!isWriteable()) return - - if (user.hasReport(userProfile(), type)) { - // has already reported this note - return - } - - val event = ReportEvent.create(user.pubkeyHex, type, loggedIn.privKey!!) - Client.send(event) - LocalCache.consume(event, null) - } - - fun delete(note: Note) { - delete(listOf(note)) - } - - fun delete(notes: List) { - if (!isWriteable()) return - - val myNotes = notes.filter { it.author == userProfile() }.map { it.idHex } - - if (myNotes.isNotEmpty()) { - val event = DeletionEvent.create(myNotes, loggedIn.privKey!!) - Client.send(event) - LocalCache.consume(event) - } - } - - fun boost(note: Note) { - if (!isWriteable()) return - - if (note.hasBoostedInTheLast5Minutes(userProfile())) { - // has already bosted in the past 5mins - return - } - - note.event?.let { - val event = RepostEvent.create(it, loggedIn.privKey!!) - Client.send(event) - LocalCache.consume(event) - } - } - - fun broadcast(note: Note) { - note.event?.let { - Client.send(it) - } - } - - fun follow(user: User) { - if (!isWriteable()) return - - val contactList = userProfile().latestContactList - val followingUsers = contactList?.follows() ?: emptyList() - val followingTags = contactList?.unverifiedFollowTagSet() ?: emptyList() - - val event = if (contactList != null) { - ContactListEvent.create( - followingUsers.plus(Contact(user.pubkeyHex, null)), - followingTags, - contactList.relays(), - loggedIn.privKey!! - ) - } else { - val relays = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) } - ContactListEvent.create( - listOf(Contact(user.pubkeyHex, null)), - followingTags, - relays, - loggedIn.privKey!! - ) - } - - Client.send(event) - LocalCache.consume(event) - } - - fun follow(tag: String) { - if (!isWriteable()) return - - val contactList = userProfile().latestContactList - val followingUsers = contactList?.follows() ?: emptyList() - val followingTags = contactList?.unverifiedFollowTagSet() ?: emptyList() - - val event = if (contactList != null) { - ContactListEvent.create( - followingUsers, - followingTags.plus(tag), - contactList.relays(), - loggedIn.privKey!! - ) - } else { - val relays = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) } - ContactListEvent.create( - followingUsers, - followingTags.plus(tag), - relays, - loggedIn.privKey!! - ) - } - - Client.send(event) - LocalCache.consume(event) - } - - fun unfollow(user: User) { - if (!isWriteable()) return - - val contactList = userProfile().latestContactList - val followingUsers = contactList?.follows() ?: emptyList() - val followingTags = contactList?.unverifiedFollowTagSet() ?: emptyList() - - if (contactList != null && (followingUsers.isNotEmpty() || followingTags.isNotEmpty())) { - val event = ContactListEvent.create( - followingUsers.filter { it.pubKeyHex != user.pubkeyHex }, - followingTags, - contactList.relays(), - loggedIn.privKey!! - ) - - Client.send(event) - LocalCache.consume(event) - } - } - - fun unfollow(tag: String) { - if (!isWriteable()) return - - val contactList = userProfile().latestContactList - val followingUsers = contactList?.follows() ?: emptyList() - val followingTags = contactList?.unverifiedFollowTagSet() ?: emptyList() - - if (contactList != null && (followingUsers.isNotEmpty() || followingTags.isNotEmpty())) { - val event = ContactListEvent.create( - followingUsers, - followingTags.filter { !it.equals(tag, ignoreCase = true) }, - contactList.relays(), - loggedIn.privKey!! - ) - - Client.send(event) - LocalCache.consume(event) - } - } - - fun sendPost(message: String, replyTo: List?, mentions: List?, tags: List? = null) { - if (!isWriteable()) return - - val repliesToHex = replyTo?.filter { it.address() == null }?.map { it.idHex } - val mentionsHex = mentions?.map { it.pubkeyHex } - val addresses = replyTo?.mapNotNull { it.address() } - - val signedEvent = TextNoteEvent.create( - msg = message, - replyTos = repliesToHex, - mentions = mentionsHex, - addresses = addresses, - extraTags = tags, - privateKey = loggedIn.privKey!! - ) - - Client.send(signedEvent) - LocalCache.consume(signedEvent) - } - - fun sendPoll( - message: String, - replyTo: List?, - mentions: List?, - pollOptions: Map, - 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("Sending new 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 - - val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null } - val mentionsHex = mentions?.map { it.pubkeyHex } - - val signedEvent = ChannelMessageEvent.create( - message = message, - channel = toChannel, - replyTos = repliesToHex, - mentions = mentionsHex, - privateKey = loggedIn.privKey!! - ) - Client.send(signedEvent) - LocalCache.consume(signedEvent, null) - } - - fun sendPrivateMessage(message: String, toUser: String, replyingTo: Note? = null) { - if (!isWriteable()) return - val user = LocalCache.users[toUser] ?: return - - val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null } - val mentionsHex = emptyList() - - val signedEvent = PrivateDmEvent.create( - recipientPubKey = user.pubkey(), - publishedRecipientPubKey = user.pubkey(), - msg = message, - replyTos = repliesToHex, - mentions = mentionsHex, - privateKey = loggedIn.privKey!!, - advertiseNip18 = false - ) - Client.send(signedEvent) - LocalCache.consume(signedEvent, null) - } - - fun sendCreateNewChannel(name: String, about: String, picture: String) { - if (!isWriteable()) return - - val metadata = ChannelCreateEvent.ChannelData( - name, - about, - picture - ) - - val event = ChannelCreateEvent.create( - channelInfo = metadata, - privateKey = loggedIn.privKey!! - ) - - Client.send(event) - LocalCache.consume(event) - - joinChannel(event.id) - } - - fun addPrivateBookmark(note: Note) { - if (!isWriteable()) return - - val bookmarks = userProfile().latestBookmarkList - - val event = BookmarkListEvent.create( - "bookmark", - bookmarks?.taggedEvents() ?: emptyList(), - bookmarks?.taggedUsers() ?: emptyList(), - bookmarks?.taggedAddresses() ?: emptyList(), - - bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!)?.plus(note.idHex) ?: listOf(note.idHex), - bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(), - bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!) ?: emptyList(), - - loggedIn.privKey!! - ) - - Client.send(event) - LocalCache.consume(event) - } - - fun addPublicBookmark(note: Note) { - if (!isWriteable()) return - - val bookmarks = userProfile().latestBookmarkList - - val event = BookmarkListEvent.create( - "bookmark", - bookmarks?.taggedEvents()?.plus(note.idHex) ?: listOf(note.idHex), - bookmarks?.taggedUsers() ?: emptyList(), - bookmarks?.taggedAddresses() ?: emptyList(), - - bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!) ?: emptyList(), - bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(), - bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!) ?: emptyList(), - - loggedIn.privKey!! - ) - - Client.send(event) - LocalCache.consume(event) - } - - fun removePrivateBookmark(note: Note) { - if (!isWriteable()) return - - val bookmarks = userProfile().latestBookmarkList - - val event = BookmarkListEvent.create( - "bookmark", - bookmarks?.taggedEvents() ?: emptyList(), - bookmarks?.taggedUsers() ?: emptyList(), - bookmarks?.taggedAddresses() ?: emptyList(), - - bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!)?.minus(note.idHex) ?: listOf(), - bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(), - bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!) ?: emptyList(), - - loggedIn.privKey!! - ) - - Client.send(event) - LocalCache.consume(event) - } - - fun removePublicBookmark(note: Note) { - if (!isWriteable()) return - - val bookmarks = userProfile().latestBookmarkList - - val event = BookmarkListEvent.create( - "bookmark", - bookmarks?.taggedEvents()?.minus(note.idHex), - bookmarks?.taggedUsers() ?: emptyList(), - bookmarks?.taggedAddresses() ?: emptyList(), - - bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!) ?: emptyList(), - bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(), - bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!) ?: emptyList(), - - loggedIn.privKey!! - ) - - Client.send(event) - LocalCache.consume(event) - } - - fun isInPrivateBookmarks(note: Note): Boolean { - if (!isWriteable()) return false - - if (note is AddressableNote) { - return userProfile().latestBookmarkList?.privateTaggedAddresses(loggedIn.privKey!!) - ?.contains(note.address) == true - } else { - return userProfile().latestBookmarkList?.privateTaggedEvents(loggedIn.privKey!!) - ?.contains(note.idHex) == true - } - } - - fun isInPublicBookmarks(note: Note): Boolean { - if (!isWriteable()) return false - - if (note is AddressableNote) { - return userProfile().latestBookmarkList?.taggedAddresses()?.contains(note.address) == true - } else { - return userProfile().latestBookmarkList?.taggedEvents()?.contains(note.idHex) == true - } - } - - fun joinChannel(idHex: String) { - followingChannels = followingChannels + idHex - live.invalidateData() - - saveable.invalidateData() - } - - fun leaveChannel(idHex: String) { - followingChannels = followingChannels - idHex - live.invalidateData() - - saveable.invalidateData() - } - - fun hideUser(pubkeyHex: String) { - hiddenUsers = hiddenUsers + pubkeyHex - live.invalidateData() - saveable.invalidateData() - } - - fun showUser(pubkeyHex: String) { - hiddenUsers = hiddenUsers - pubkeyHex - transientHiddenUsers = transientHiddenUsers - pubkeyHex - live.invalidateData() - saveable.invalidateData() - } - - fun changeZapAmounts(newAmounts: List) { - zapAmountChoices = newAmounts - live.invalidateData() - saveable.invalidateData() - } - - fun changeZapPaymentRequest(newServer: Nip47URI?) { - zapPaymentRequest = newServer - live.invalidateData() - saveable.invalidateData() - } - - fun sendChangeChannel(name: String, about: String, picture: String, channel: Channel) { - if (!isWriteable()) return - - val metadata = ChannelCreateEvent.ChannelData( - name, - about, - picture - ) - - val event = ChannelMetadataEvent.create( - newChannelInfo = metadata, - originalChannelIdHex = channel.idHex, - privateKey = loggedIn.privKey!! - ) - - Client.send(event) - LocalCache.consume(event) - - joinChannel(event.id) - } - - fun decryptContent(note: Note): String? { - val event = note.event - return if (event is PrivateDmEvent && loggedIn.privKey != null) { - var pubkeyToUse = event.pubKey - - val recepientPK = event.recipientPubKey() - - if (note.author == userProfile() && recepientPK != null) { - pubkeyToUse = recepientPK - } - - event.plainContent(loggedIn.privKey!!, pubkeyToUse.toByteArray()) - } else { - event?.content() - } - } - - fun addDontTranslateFrom(languageCode: String) { - dontTranslateFrom = dontTranslateFrom.plus(languageCode) - liveLanguages.invalidateData() - - saveable.invalidateData() - } - - fun updateTranslateTo(languageCode: String) { - translateTo = languageCode - liveLanguages.invalidateData() - - saveable.invalidateData() - } - - fun prefer(source: String, target: String, preference: String) { - languagePreferences = languagePreferences + Pair("$source,$target", preference) - saveable.invalidateData() - } - - fun preferenceBetween(source: String, target: String): String? { - return languagePreferences.get("$source,$target") - } - - private fun updateContactListTo(newContactList: ContactListEvent?) { - if (newContactList?.unverifiedFollowKeySet().isNullOrEmpty()) return - - // Events might be different objects, we have to compare their ids. - if (backupContactList?.id != newContactList?.id) { - backupContactList = newContactList - saveable.invalidateData() - } - } - - // Takes a User's relay list and adds the types of feeds they are active for. - fun activeRelays(): Array? { - var usersRelayList = userProfile().latestContactList?.relays()?.map { - val localFeedTypes = localRelays.firstOrNull() { localRelay -> localRelay.url == it.key }?.feedTypes ?: FeedType.values().toSet() - Relay(it.key, it.value.read, it.value.write, localFeedTypes) - } ?: return null - - // Ugly, but forces nostr.band as the only search-supporting relay today. - // TODO: Remove when search becomes more available. - if (usersRelayList.none { it.activeTypes.contains(FeedType.SEARCH) }) { - usersRelayList = usersRelayList + Relay( - Constants.forcedRelayForSearch.url, - Constants.forcedRelayForSearch.read, - Constants.forcedRelayForSearch.write, - Constants.forcedRelayForSearch.feedTypes - ) - } - - return usersRelayList.toTypedArray() - } - - fun convertLocalRelays(): Array { - return localRelays.map { - Relay(it.url, it.read, it.write, it.feedTypes) - }.toTypedArray() - } - - fun reconnectIfRelaysHaveChanged() { - val newRelaySet = activeRelays() ?: convertLocalRelays() - if (!Client.isSameRelaySetConfig(newRelaySet)) { - Client.disconnect() - Client.connect(newRelaySet) - RelayPool.requestAndWatch() - } - } - - fun isHidden(user: User) = user.pubkeyHex in hiddenUsers || user.pubkeyHex in transientHiddenUsers - - fun followingKeySet(): Set { - return userProfile().cachedFollowingKeySet() ?: emptySet() - } - - fun followingTagSet(): Set { - return userProfile().cachedFollowingTagSet() ?: emptySet() - } - - fun isAcceptable(user: User): Boolean { - return !isHidden(user) && // if user hasn't hided this author - user.reportsBy(userProfile()).isEmpty() && // if user has not reported this post - user.countReportAuthorsBy(followingKeySet()) < 5 - } - - fun isAcceptableDirect(note: Note): Boolean { - return note.reportsBy(userProfile()).isEmpty() && // if user has not reported this post - note.countReportAuthorsBy(followingKeySet()) < 5 // if it has 5 reports by reliable users - } - - fun isFollowing(user: User): Boolean { - return user.pubkeyHex in followingKeySet() - } - - fun isAcceptable(note: Note): Boolean { - return note.author?.let { isAcceptable(it) } ?: true && // if user hasn't hided this author - isAcceptableDirect(note) && - ( - note.event !is RepostEvent || - (note.replyTo?.firstOrNull { isAcceptableDirect(it) } != null) - ) // is not a reaction about a blocked post - } - - fun getRelevantReports(note: Note): Set { - val followsPlusMe = userProfile().latestContactList?.verifiedFollowKeySetAndMe ?: emptySet() - - val innerReports = if (note.event is RepostEvent) { - note.replyTo?.map { getRelevantReports(it) }?.flatten() ?: emptyList() - } else { - emptyList() - } - - return ( - note.reportsBy(followsPlusMe) + - ( - note.author?.reportsBy(followsPlusMe) ?: emptyList() - ) + innerReports - ).toSet() - } - - fun saveRelayList(value: List) { - localRelays = value.toSet() - sendNewRelayList(value.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) }) - - saveable.invalidateData() - } - - fun setHideDeleteRequestDialog() { - hideDeleteRequestDialog = true - saveable.invalidateData() - } - - fun setHideBlockAlertDialog() { - hideBlockAlertDialog = true - saveable.invalidateData() - } - - init { - backupContactList?.let { - println("Loading saved contacts ${it.toJson()}") - if (userProfile().latestContactList == null) { - LocalCache.consume(it) - } - } - - // Observes relays to restart connections - userProfile().live().relays.observeForever { - GlobalScope.launch(Dispatchers.IO) { - reconnectIfRelaysHaveChanged() - } - } - - // saves contact list for the next time. - userProfile().live().follows.observeForever { - updateContactListTo(userProfile().latestContactList) - } - - // imports transient blocks due to spam. - LocalCache.antiSpam.liveSpam.observeForever { - GlobalScope.launch(Dispatchers.IO) { - it.cache.spamMessages.snapshot().values.forEach { - if (it.pubkeyHex !in transientHiddenUsers && it.duplicatedMessages.size >= 5) { - val userToBlock = LocalCache.getOrCreateUser(it.pubkeyHex) - if (userToBlock != userProfile() && userToBlock.pubkeyHex !in followingKeySet()) { - transientHiddenUsers = transientHiddenUsers + it.pubkeyHex - } - } - } - } - } - } -} - -class AccountLiveData(private val account: Account) : LiveData(AccountState(account)) { - // Refreshes observers in batches. - private val bundler = BundledUpdate(300, Dispatchers.Default) { - if (hasActiveObservers()) { - refresh() - } - } - - fun invalidateData() { - bundler.invalidate() - } - - fun refresh() { - postValue(AccountState(account)) - } -} - -class AccountState(val account: Account) +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.* +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 com.vitorpamplona.amethyst.ui.components.BundledUpdate +import com.vitorpamplona.amethyst.ui.note.Nip47URI +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import nostr.postr.Persona +import java.util.Locale + +val DefaultChannels = setOf( + "25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb", // -> Anigma's Nostr + "42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5" // -> Amethyst's Group +) + +fun getLanguagesSpokenByUser(): Set { + val languageList = ConfigurationCompat.getLocales(Resources.getSystem().getConfiguration()) + val codedList = mutableSetOf() + for (i in 0 until languageList.size()) { + languageList.get(i)?.let { codedList.add(it.language) } + } + return codedList +} + +@OptIn(DelicateCoroutinesApi::class) +class Account( + val loggedIn: Persona, + var followingChannels: Set = DefaultChannels, + var hiddenUsers: Set = setOf(), + var localRelays: Set = Constants.defaultRelays.toSet(), + var dontTranslateFrom: Set = getLanguagesSpokenByUser(), + var languagePreferences: Map = mapOf(), + var translateTo: String = Locale.getDefault().language, + var zapAmountChoices: List = listOf(500L, 1000L, 5000L), + var zapPaymentRequest: Nip47URI? = null, + var hideDeleteRequestDialog: Boolean = false, + var hideBlockAlertDialog: Boolean = false, + var backupContactList: ContactListEvent? = null +) { + var transientHiddenUsers: Set = setOf() + + // Observers line up here. + val live: AccountLiveData = AccountLiveData(this) + val liveLanguages: AccountLiveData = AccountLiveData(this) + val saveable: AccountLiveData = AccountLiveData(this) + + fun userProfile(): User { + return LocalCache.getOrCreateUser(loggedIn.pubKey.toHexKey()) + } + + fun followingChannels(): List { + return followingChannels.map { LocalCache.getOrCreateChannel(it) } + } + + fun hiddenUsers(): List { + return (hiddenUsers + transientHiddenUsers).map { LocalCache.getOrCreateUser(it) } + } + + fun isWriteable(): Boolean { + return loggedIn.privKey != null + } + + fun sendNewRelayList(relays: Map) { + if (!isWriteable()) return + + val contactList = userProfile().latestContactList + val follows = contactList?.follows() ?: emptyList() + val followsTags = contactList?.unverifiedFollowTagSet() ?: emptyList() + + if (contactList != null && follows.isNotEmpty()) { + val event = ContactListEvent.create( + follows, + followsTags, + relays, + loggedIn.privKey!! + ) + + Client.send(event) + LocalCache.consume(event) + } else { + val event = ContactListEvent.create(listOf(), listOf(), relays, loggedIn.privKey!!) + + // Keep this local to avoid erasing a good contact list. + // Client.send(event) + LocalCache.consume(event) + } + } + + fun sendNewUserMetadata(toString: String, identities: List) { + if (!isWriteable()) return + + loggedIn.privKey?.let { + val event = MetadataEvent.create(toString, identities, loggedIn.privKey!!) + Client.send(event) + LocalCache.consume(event) + } + } + + fun reactionTo(note: Note): List { + return note.reactedBy(userProfile(), "+") + } + + fun hasBoosted(note: Note): Boolean { + return boostsTo(note).isNotEmpty() + } + + fun boostsTo(note: Note): List { + return note.boostedBy(userProfile()) + } + + fun hasReacted(note: Note): Boolean { + return note.hasReacted(userProfile(), "+") + } + + fun reactTo(note: Note) { + if (!isWriteable()) return + + if (hasReacted(note)) { + // has already liked this note + return + } + + note.event?.let { + val event = ReactionEvent.createLike(it, loggedIn.privKey!!) + Client.send(event) + LocalCache.consume(event) + } + } + + fun createZapRequestFor(note: Note, pollOption: Int?, message: String = ""): LnZapRequestEvent? { + if (!isWriteable()) return null + + note.event?.let { event -> + return LnZapRequestEvent.create( + event, + userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } + ?: localRelays.map { it.url }.toSet(), + loggedIn.privKey!!, + pollOption, + message + ) + } + return null + } + + fun hasWalletConnectSetup(): Boolean { + return zapPaymentRequest != null + } + + fun sendZapPaymentRequestFor(lnInvoice: String) { + if (!isWriteable()) return + + zapPaymentRequest?.let { + val event = LnZapPaymentRequestEvent.create(lnInvoice, it.pubKeyHex, it.secret?.toByteArray() ?: loggedIn.privKey!!) + + Client.send(event, it.relayUri) + } + } + + fun createZapRequestFor(user: User): LnZapRequestEvent? { + return createZapRequestFor(user) + } + + fun createZapRequestFor(userPubKeyHex: String, message: String = ""): LnZapRequestEvent? { + if (!isWriteable()) return null + + return LnZapRequestEvent.create( + userPubKeyHex, + userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } ?: localRelays.map { it.url }.toSet(), + loggedIn.privKey!!, + message + ) + } + + fun report(note: Note, type: ReportEvent.ReportType, content: String = "") { + if (!isWriteable()) return + + if (note.hasReacted(userProfile(), "⚠️")) { + // has already liked this note + return + } + + note.event?.let { + val event = ReactionEvent.createWarning(it, loggedIn.privKey!!) + Client.send(event) + LocalCache.consume(event) + } + + note.event?.let { + val event = ReportEvent.create(it, type, loggedIn.privKey!!, content = content) + Client.send(event) + LocalCache.consume(event, null) + } + } + + fun report(user: User, type: ReportEvent.ReportType) { + if (!isWriteable()) return + + if (user.hasReport(userProfile(), type)) { + // has already reported this note + return + } + + val event = ReportEvent.create(user.pubkeyHex, type, loggedIn.privKey!!) + Client.send(event) + LocalCache.consume(event, null) + } + + fun delete(note: Note) { + delete(listOf(note)) + } + + fun delete(notes: List) { + if (!isWriteable()) return + + val myNotes = notes.filter { it.author == userProfile() }.map { it.idHex } + + if (myNotes.isNotEmpty()) { + val event = DeletionEvent.create(myNotes, loggedIn.privKey!!) + Client.send(event) + LocalCache.consume(event) + } + } + + fun boost(note: Note) { + if (!isWriteable()) return + + if (note.hasBoostedInTheLast5Minutes(userProfile())) { + // has already bosted in the past 5mins + return + } + + note.event?.let { + val event = RepostEvent.create(it, loggedIn.privKey!!) + Client.send(event) + LocalCache.consume(event) + } + } + + fun broadcast(note: Note) { + note.event?.let { + Client.send(it) + } + } + + fun follow(user: User) { + if (!isWriteable()) return + + val contactList = userProfile().latestContactList + val followingUsers = contactList?.follows() ?: emptyList() + val followingTags = contactList?.unverifiedFollowTagSet() ?: emptyList() + + val event = if (contactList != null) { + ContactListEvent.create( + followingUsers.plus(Contact(user.pubkeyHex, null)), + followingTags, + contactList.relays(), + loggedIn.privKey!! + ) + } else { + val relays = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) } + ContactListEvent.create( + listOf(Contact(user.pubkeyHex, null)), + followingTags, + relays, + loggedIn.privKey!! + ) + } + + Client.send(event) + LocalCache.consume(event) + } + + fun follow(tag: String) { + if (!isWriteable()) return + + val contactList = userProfile().latestContactList + val followingUsers = contactList?.follows() ?: emptyList() + val followingTags = contactList?.unverifiedFollowTagSet() ?: emptyList() + + val event = if (contactList != null) { + ContactListEvent.create( + followingUsers, + followingTags.plus(tag), + contactList.relays(), + loggedIn.privKey!! + ) + } else { + val relays = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) } + ContactListEvent.create( + followingUsers, + followingTags.plus(tag), + relays, + loggedIn.privKey!! + ) + } + + Client.send(event) + LocalCache.consume(event) + } + + fun unfollow(user: User) { + if (!isWriteable()) return + + val contactList = userProfile().latestContactList + val followingUsers = contactList?.follows() ?: emptyList() + val followingTags = contactList?.unverifiedFollowTagSet() ?: emptyList() + + if (contactList != null && (followingUsers.isNotEmpty() || followingTags.isNotEmpty())) { + val event = ContactListEvent.create( + followingUsers.filter { it.pubKeyHex != user.pubkeyHex }, + followingTags, + contactList.relays(), + loggedIn.privKey!! + ) + + Client.send(event) + LocalCache.consume(event) + } + } + + fun unfollow(tag: String) { + if (!isWriteable()) return + + val contactList = userProfile().latestContactList + val followingUsers = contactList?.follows() ?: emptyList() + val followingTags = contactList?.unverifiedFollowTagSet() ?: emptyList() + + if (contactList != null && (followingUsers.isNotEmpty() || followingTags.isNotEmpty())) { + val event = ContactListEvent.create( + followingUsers, + followingTags.filter { !it.equals(tag, ignoreCase = true) }, + contactList.relays(), + loggedIn.privKey!! + ) + + Client.send(event) + LocalCache.consume(event) + } + } + + fun sendPost(message: String, replyTo: List?, mentions: List?, tags: List? = null) { + if (!isWriteable()) return + + val repliesToHex = replyTo?.filter { it.address() == null }?.map { it.idHex } + val mentionsHex = mentions?.map { it.pubkeyHex } + val addresses = replyTo?.mapNotNull { it.address() } + + val signedEvent = TextNoteEvent.create( + msg = message, + replyTos = repliesToHex, + mentions = mentionsHex, + addresses = addresses, + extraTags = tags, + privateKey = loggedIn.privKey!! + ) + + Client.send(signedEvent) + LocalCache.consume(signedEvent) + } + + fun sendPoll( + message: String, + replyTo: List?, + mentions: List?, + pollOptions: Map, + 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("Sending new 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 + + val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null } + val mentionsHex = mentions?.map { it.pubkeyHex } + + val signedEvent = ChannelMessageEvent.create( + message = message, + channel = toChannel, + replyTos = repliesToHex, + mentions = mentionsHex, + privateKey = loggedIn.privKey!! + ) + Client.send(signedEvent) + LocalCache.consume(signedEvent, null) + } + + fun sendPrivateMessage(message: String, toUser: String, replyingTo: Note? = null) { + if (!isWriteable()) return + val user = LocalCache.users[toUser] ?: return + + val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null } + val mentionsHex = emptyList() + + val signedEvent = PrivateDmEvent.create( + recipientPubKey = user.pubkey(), + publishedRecipientPubKey = user.pubkey(), + msg = message, + replyTos = repliesToHex, + mentions = mentionsHex, + privateKey = loggedIn.privKey!!, + advertiseNip18 = false + ) + Client.send(signedEvent) + LocalCache.consume(signedEvent, null) + } + + fun sendCreateNewChannel(name: String, about: String, picture: String) { + if (!isWriteable()) return + + val metadata = ChannelCreateEvent.ChannelData( + name, + about, + picture + ) + + val event = ChannelCreateEvent.create( + channelInfo = metadata, + privateKey = loggedIn.privKey!! + ) + + Client.send(event) + LocalCache.consume(event) + + joinChannel(event.id) + } + + fun addPrivateBookmark(note: Note) { + if (!isWriteable()) return + + val bookmarks = userProfile().latestBookmarkList + + val event = BookmarkListEvent.create( + "bookmark", + bookmarks?.taggedEvents() ?: emptyList(), + bookmarks?.taggedUsers() ?: emptyList(), + bookmarks?.taggedAddresses() ?: emptyList(), + + bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!)?.plus(note.idHex) ?: listOf(note.idHex), + bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(), + bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!) ?: emptyList(), + + loggedIn.privKey!! + ) + + Client.send(event) + LocalCache.consume(event) + } + + fun addPublicBookmark(note: Note) { + if (!isWriteable()) return + + val bookmarks = userProfile().latestBookmarkList + + val event = BookmarkListEvent.create( + "bookmark", + bookmarks?.taggedEvents()?.plus(note.idHex) ?: listOf(note.idHex), + bookmarks?.taggedUsers() ?: emptyList(), + bookmarks?.taggedAddresses() ?: emptyList(), + + bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!) ?: emptyList(), + bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(), + bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!) ?: emptyList(), + + loggedIn.privKey!! + ) + + Client.send(event) + LocalCache.consume(event) + } + + fun removePrivateBookmark(note: Note) { + if (!isWriteable()) return + + val bookmarks = userProfile().latestBookmarkList + + val event = BookmarkListEvent.create( + "bookmark", + bookmarks?.taggedEvents() ?: emptyList(), + bookmarks?.taggedUsers() ?: emptyList(), + bookmarks?.taggedAddresses() ?: emptyList(), + + bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!)?.minus(note.idHex) ?: listOf(), + bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(), + bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!) ?: emptyList(), + + loggedIn.privKey!! + ) + + Client.send(event) + LocalCache.consume(event) + } + + fun removePublicBookmark(note: Note) { + if (!isWriteable()) return + + val bookmarks = userProfile().latestBookmarkList + + val event = BookmarkListEvent.create( + "bookmark", + bookmarks?.taggedEvents()?.minus(note.idHex), + bookmarks?.taggedUsers() ?: emptyList(), + bookmarks?.taggedAddresses() ?: emptyList(), + + bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!) ?: emptyList(), + bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(), + bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!) ?: emptyList(), + + loggedIn.privKey!! + ) + + Client.send(event) + LocalCache.consume(event) + } + + fun isInPrivateBookmarks(note: Note): Boolean { + if (!isWriteable()) return false + + if (note is AddressableNote) { + return userProfile().latestBookmarkList?.privateTaggedAddresses(loggedIn.privKey!!) + ?.contains(note.address) == true + } else { + return userProfile().latestBookmarkList?.privateTaggedEvents(loggedIn.privKey!!) + ?.contains(note.idHex) == true + } + } + + fun isInPublicBookmarks(note: Note): Boolean { + if (!isWriteable()) return false + + if (note is AddressableNote) { + return userProfile().latestBookmarkList?.taggedAddresses()?.contains(note.address) == true + } else { + return userProfile().latestBookmarkList?.taggedEvents()?.contains(note.idHex) == true + } + } + + fun joinChannel(idHex: String) { + followingChannels = followingChannels + idHex + live.invalidateData() + + saveable.invalidateData() + } + + fun leaveChannel(idHex: String) { + followingChannels = followingChannels - idHex + live.invalidateData() + + saveable.invalidateData() + } + + fun hideUser(pubkeyHex: String) { + hiddenUsers = hiddenUsers + pubkeyHex + live.invalidateData() + saveable.invalidateData() + } + + fun showUser(pubkeyHex: String) { + hiddenUsers = hiddenUsers - pubkeyHex + transientHiddenUsers = transientHiddenUsers - pubkeyHex + live.invalidateData() + saveable.invalidateData() + } + + fun changeZapAmounts(newAmounts: List) { + zapAmountChoices = newAmounts + live.invalidateData() + saveable.invalidateData() + } + + fun changeZapPaymentRequest(newServer: Nip47URI?) { + zapPaymentRequest = newServer + live.invalidateData() + saveable.invalidateData() + } + + fun sendChangeChannel(name: String, about: String, picture: String, channel: Channel) { + if (!isWriteable()) return + + val metadata = ChannelCreateEvent.ChannelData( + name, + about, + picture + ) + + val event = ChannelMetadataEvent.create( + newChannelInfo = metadata, + originalChannelIdHex = channel.idHex, + privateKey = loggedIn.privKey!! + ) + + Client.send(event) + LocalCache.consume(event) + + joinChannel(event.id) + } + + fun decryptContent(note: Note): String? { + val event = note.event + return if (event is PrivateDmEvent && loggedIn.privKey != null) { + var pubkeyToUse = event.pubKey + + val recepientPK = event.recipientPubKey() + + if (note.author == userProfile() && recepientPK != null) { + pubkeyToUse = recepientPK + } + + event.plainContent(loggedIn.privKey!!, pubkeyToUse.toByteArray()) + } else { + event?.content() + } + } + + fun addDontTranslateFrom(languageCode: String) { + dontTranslateFrom = dontTranslateFrom.plus(languageCode) + liveLanguages.invalidateData() + + saveable.invalidateData() + } + + fun updateTranslateTo(languageCode: String) { + translateTo = languageCode + liveLanguages.invalidateData() + + saveable.invalidateData() + } + + fun prefer(source: String, target: String, preference: String) { + languagePreferences = languagePreferences + Pair("$source,$target", preference) + saveable.invalidateData() + } + + fun preferenceBetween(source: String, target: String): String? { + return languagePreferences.get("$source,$target") + } + + private fun updateContactListTo(newContactList: ContactListEvent?) { + if (newContactList?.unverifiedFollowKeySet().isNullOrEmpty()) return + + // Events might be different objects, we have to compare their ids. + if (backupContactList?.id != newContactList?.id) { + backupContactList = newContactList + saveable.invalidateData() + } + } + + // Takes a User's relay list and adds the types of feeds they are active for. + fun activeRelays(): Array? { + var usersRelayList = userProfile().latestContactList?.relays()?.map { + val localFeedTypes = localRelays.firstOrNull() { localRelay -> localRelay.url == it.key }?.feedTypes ?: FeedType.values().toSet() + Relay(it.key, it.value.read, it.value.write, localFeedTypes) + } ?: return null + + // Ugly, but forces nostr.band as the only search-supporting relay today. + // TODO: Remove when search becomes more available. + if (usersRelayList.none { it.activeTypes.contains(FeedType.SEARCH) }) { + usersRelayList = usersRelayList + Relay( + Constants.forcedRelayForSearch.url, + Constants.forcedRelayForSearch.read, + Constants.forcedRelayForSearch.write, + Constants.forcedRelayForSearch.feedTypes + ) + } + + return usersRelayList.toTypedArray() + } + + fun convertLocalRelays(): Array { + return localRelays.map { + Relay(it.url, it.read, it.write, it.feedTypes) + }.toTypedArray() + } + + fun reconnectIfRelaysHaveChanged() { + val newRelaySet = activeRelays() ?: convertLocalRelays() + if (!Client.isSameRelaySetConfig(newRelaySet)) { + Client.disconnect() + Client.connect(newRelaySet) + RelayPool.requestAndWatch() + } + } + + fun isHidden(user: User) = user.pubkeyHex in hiddenUsers || user.pubkeyHex in transientHiddenUsers + + fun followingKeySet(): Set { + return userProfile().cachedFollowingKeySet() ?: emptySet() + } + + fun followingTagSet(): Set { + return userProfile().cachedFollowingTagSet() ?: emptySet() + } + + fun isAcceptable(user: User): Boolean { + return !isHidden(user) && // if user hasn't hided this author + user.reportsBy(userProfile()).isEmpty() && // if user has not reported this post + user.countReportAuthorsBy(followingKeySet()) < 5 + } + + fun isAcceptableDirect(note: Note): Boolean { + return note.reportsBy(userProfile()).isEmpty() && // if user has not reported this post + note.countReportAuthorsBy(followingKeySet()) < 5 // if it has 5 reports by reliable users + } + + fun isFollowing(user: User): Boolean { + return user.pubkeyHex in followingKeySet() + } + + fun isAcceptable(note: Note): Boolean { + return note.author?.let { isAcceptable(it) } ?: true && // if user hasn't hided this author + isAcceptableDirect(note) && + ( + note.event !is RepostEvent || + (note.replyTo?.firstOrNull { isAcceptableDirect(it) } != null) + ) // is not a reaction about a blocked post + } + + fun getRelevantReports(note: Note): Set { + val followsPlusMe = userProfile().latestContactList?.verifiedFollowKeySetAndMe ?: emptySet() + + val innerReports = if (note.event is RepostEvent) { + note.replyTo?.map { getRelevantReports(it) }?.flatten() ?: emptyList() + } else { + emptyList() + } + + return ( + note.reportsBy(followsPlusMe) + + ( + note.author?.reportsBy(followsPlusMe) ?: emptyList() + ) + innerReports + ).toSet() + } + + fun saveRelayList(value: List) { + localRelays = value.toSet() + sendNewRelayList(value.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) }) + + saveable.invalidateData() + } + + fun setHideDeleteRequestDialog() { + hideDeleteRequestDialog = true + saveable.invalidateData() + } + + fun setHideBlockAlertDialog() { + hideBlockAlertDialog = true + saveable.invalidateData() + } + + init { + backupContactList?.let { + println("Loading saved contacts ${it.toJson()}") + if (userProfile().latestContactList == null) { + LocalCache.consume(it) + } + } + + // Observes relays to restart connections + userProfile().live().relays.observeForever { + GlobalScope.launch(Dispatchers.IO) { + reconnectIfRelaysHaveChanged() + } + } + + // saves contact list for the next time. + userProfile().live().follows.observeForever { + updateContactListTo(userProfile().latestContactList) + } + + // imports transient blocks due to spam. + LocalCache.antiSpam.liveSpam.observeForever { + GlobalScope.launch(Dispatchers.IO) { + it.cache.spamMessages.snapshot().values.forEach { + if (it.pubkeyHex !in transientHiddenUsers && it.duplicatedMessages.size >= 5) { + val userToBlock = LocalCache.getOrCreateUser(it.pubkeyHex) + if (userToBlock != userProfile() && userToBlock.pubkeyHex !in followingKeySet()) { + transientHiddenUsers = transientHiddenUsers + it.pubkeyHex + } + } + } + } + } + } +} + +class AccountLiveData(private val account: Account) : LiveData(AccountState(account)) { + // Refreshes observers in batches. + private val bundler = BundledUpdate(300, Dispatchers.Default) { + if (hasActiveObservers()) { + refresh() + } + } + + fun invalidateData() { + bundler.invalidate() + } + + fun refresh() { + postValue(AccountState(account)) + } +} + +class AccountState(val account: Account) 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 d92584ed6b..c019bb69d1 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 @@ -1,73 +1,66 @@ -package com.vitorpamplona.amethyst.service.model - -import android.util.Log -import com.vitorpamplona.amethyst.model.HexKey -import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil -import com.vitorpamplona.amethyst.service.relays.Client -import java.math.BigDecimal - -class LnZapEvent( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: List>, - content: String, - sig: HexKey -) : LnZapEventInterface, Event(id, pubKey, createdAt, kind, tags, content, sig) { - - override fun zappedPost() = tags - .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" } - .mapNotNull { it.getOrNull(1) } - - override fun amount(): BigDecimal? { - return amount - } - - // Keeps this as a field because it's a heavier function used everywhere. - val amount by lazy { - try { - lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) } - } catch (e: Exception) { - Log.e("LnZapEvent", "Failed to Parse LnInvoice ${description()}", e) - null - } - } - - override fun containedPost(): Event? = try { - description()?.let { - fromJson(it, Client.lenient) - } - } catch (e: Exception) { - Log.e("LnZapEvent", "Failed to Parse Contained Post ${description()}", e) - null - } - - private fun lnInvoice(): String? = tags - .filter { it.firstOrNull() == "bolt11" } - .mapNotNull { it.getOrNull(1) } - .firstOrNull() - - private fun description(): String? = tags - .filter { it.firstOrNull() == "description" } - .mapNotNull { it.getOrNull(1) } - .firstOrNull() - - companion object { - const val kind = 9735 - } -} +package com.vitorpamplona.amethyst.service.model + +import android.util.Log +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil +import com.vitorpamplona.amethyst.service.relays.Client +import java.math.BigDecimal + +class LnZapEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey +) : LnZapEventInterface, Event(id, pubKey, createdAt, kind, tags, content, sig) { + + override fun zappedPost() = tags + .filter { it.firstOrNull() == "e" } + .mapNotNull { it.getOrNull(1) } + + override fun zappedPollOption(): Int? = containedPost()?.tags + ?.filter { it.firstOrNull() == POLL_OPTION } + ?.getOrNull(0)?.getOrNull(1)?.toInt() + + override fun zappedAuthor() = tags + .filter { it.firstOrNull() == "p" } + .mapNotNull { it.getOrNull(1) } + + override fun amount(): BigDecimal? { + return amount + } + + // Keeps this as a field because it's a heavier function used everywhere. + val amount by lazy { + try { + lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) } + } catch (e: Exception) { + Log.e("LnZapEvent", "Failed to Parse LnInvoice ${description()}", e) + null + } + } + + override fun containedPost(): Event? = try { + description()?.let { + fromJson(it, Client.lenient) + } + } catch (e: Exception) { + Log.e("LnZapEvent", "Failed to Parse Contained Post ${description()}", e) + null + } + + private fun lnInvoice(): String? = tags + .filter { it.firstOrNull() == "bolt11" } + .mapNotNull { it.getOrNull(1) } + .firstOrNull() + + private fun description(): String? = tags + .filter { it.firstOrNull() == "description" } + .mapNotNull { it.getOrNull(1) } + .firstOrNull() + + companion object { + const val kind = 9735 + } +} 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 fae2d053b8..5452aa36ab 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 @@ -1,109 +1,106 @@ -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 LnZapRequestEvent( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: List>, - content: String, - sig: HexKey -) : Event(id, pubKey, createdAt, kind, tags, content, sig) { - fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } - fun zappedAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } - - companion object { - const val kind = 9734 - - fun create( - originalNote: EventInterface, - relays: Set, - privateKey: ByteArray, - pollOption: Int?, - message: String, - createdAt: Long = Date().time / 1000 - ): LnZapRequestEvent { - val content = message - val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() - var tags = listOf( - listOf("e", originalNote.id()), - listOf("p", originalNote.pubKey()), - listOf("relays") + relays - ) - 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) - return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) - } - - fun create( - userHex: String, - relays: Set, - privateKey: ByteArray, - pollOption: Int?, - message: String, - createdAt: Long = Date().time / 1000 - ): LnZapRequestEvent { - val content = message - val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() - var tags = listOf( - listOf("p", userHex), - listOf("relays") + relays - ) - 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) - return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) - } - } -} - -/* -{ - "pubkey": "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245", - "content": "", - "id": "d9cc14d50fcb8c27539aacf776882942c1a11ea4472f8cdec1dea82fab66279d", - "created_at": 1674164539, - "sig": "77127f636577e9029276be060332ea565deaf89ff215a494ccff16ae3f757065e2bc59b2e8c113dd407917a010b3abd36c8d7ad84c0e3ab7dab3a0b0caa9835d", - "kind": 9734, - "tags": [ - [ - "e", - "3624762a1274dd9636e0c552b53086d70bc88c165bc4dc0f9e836a1eaf86c3b8" - ], - [ - "p", - "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245" - ], - [ - "relays", - "wss://relay.damus.io", - "wss://nostr-relay.wlvs.space", - "wss://nostr.fmt.wiz.biz", - "wss://relay.nostr.bg", - "wss://nostr.oxtr.dev", - "wss://nostr.v0l.io", - "wss://brb.io", - "wss://nostr.bitcoiner.social", - "ws://monad.jb55.com:8080", - "wss://relay.snort.social" - ], - [ - "poll_option", "n" - ] - ], - "ots": // TODO -} -*/ +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 LnZapRequestEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey +) : Event(id, pubKey, createdAt, kind, tags, content, sig) { + fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } + fun zappedAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } + + companion object { + const val kind = 9734 + + fun create( + originalNote: EventInterface, + relays: Set, + privateKey: ByteArray, + pollOption: Int?, + message: String, + createdAt: Long = Date().time / 1000 + ): LnZapRequestEvent { + val content = message + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() + var tags = listOf( + listOf("e", originalNote.id()), + listOf("p", originalNote.pubKey()), + listOf("relays") + relays + ) + 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) + return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + } + + fun create( + userHex: String, + relays: Set, + privateKey: ByteArray, + message: String, + createdAt: Long = Date().time / 1000 + ): LnZapRequestEvent { + val content = message + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() + val tags = listOf( + listOf("p", userHex), + listOf("relays") + relays + ) + + val id = generateId(pubKey, createdAt, kind, tags, content) + val sig = Utils.sign(id, privateKey) + return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + } + } +} + +/* +{ + "pubkey": "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245", + "content": "", + "id": "d9cc14d50fcb8c27539aacf776882942c1a11ea4472f8cdec1dea82fab66279d", + "created_at": 1674164539, + "sig": "77127f636577e9029276be060332ea565deaf89ff215a494ccff16ae3f757065e2bc59b2e8c113dd407917a010b3abd36c8d7ad84c0e3ab7dab3a0b0caa9835d", + "kind": 9734, + "tags": [ + [ + "e", + "3624762a1274dd9636e0c552b53086d70bc88c165bc4dc0f9e836a1eaf86c3b8" + ], + [ + "p", + "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245" + ], + [ + "relays", + "wss://relay.damus.io", + "wss://nostr-relay.wlvs.space", + "wss://nostr.fmt.wiz.biz", + "wss://relay.nostr.bg", + "wss://nostr.oxtr.dev", + "wss://nostr.v0l.io", + "wss://brb.io", + "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/ZapCustomDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt index 9cbb35b50d..b0ea32a753 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt @@ -1,193 +1,194 @@ -package com.vitorpamplona.amethyst.ui.note - -import android.widget.Toast -import androidx.compose.foundation.layout.* -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.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -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.text.input.KeyboardCapitalization -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewmodel.compose.viewModel -import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.ui.actions.CloseButton -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - -class ZapOptionstViewModel : ViewModel() { - private var account: Account? = null - - var customAmount by mutableStateOf(TextFieldValue("1000")) - var customMessage by mutableStateOf(TextFieldValue("")) - - fun load(account: Account) { - this.account = account - } - - fun canSend(): Boolean { - return value() != null - } - - fun value(): Long? { - return try { - customAmount.text.trim().toLongOrNull() - } catch (e: Exception) { - null - } - } - - fun cancel() { - } -} - -@Composable -fun ZapCustomDialog(onClose: () -> Unit, account: Account, accountViewModel: AccountViewModel, baseNote: Note) { - val context = LocalContext.current - val scope = rememberCoroutineScope() - val postViewModel: ZapOptionstViewModel = viewModel() - - LaunchedEffect(account) { - postViewModel.load(account) - } - - Dialog( - onDismissRequest = { onClose() }, - properties = DialogProperties( - dismissOnClickOutside = false, - usePlatformDefaultWidth = false - ) - ) { - Surface() { - Column(modifier = Modifier.padding(10.dp)) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - CloseButton(onCancel = { - postViewModel.cancel() - onClose() - }) - - ZapButton( - isActive = postViewModel.canSend() - ) { - scope.launch(Dispatchers.IO) { - accountViewModel.zap( - baseNote, - postViewModel.value()!! * 1000L, - postViewModel.customMessage.text, - context, - onError = { - scope.launch { - Toast - .makeText(context, it, Toast.LENGTH_SHORT).show() - } - }, - onProgress = { - scope.launch(Dispatchers.Main) { - } - } - ) - } - onClose() - } - } - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 5.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - OutlinedTextField( - // stringResource(R.string.new_amount_in_sats - label = { Text(text = stringResource(id = R.string.amount_in_sats)) }, - value = postViewModel.customAmount, - onValueChange = { - postViewModel.customAmount = it - }, - keyboardOptions = KeyboardOptions.Default.copy( - capitalization = KeyboardCapitalization.None, - keyboardType = KeyboardType.Number - ), - placeholder = { - Text( - text = "100, 1000, 5000", - color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) - ) - }, - singleLine = true, - modifier = Modifier - .padding(end = 10.dp) - .weight(1f) - ) - } - Spacer(modifier = Modifier.height(5.dp)) - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 5.dp), - verticalAlignment = Alignment.CenterVertically - ) { - OutlinedTextField( - // stringResource(R.string.new_amount_in_sats - label = { Text(text = stringResource(id = R.string.custom_zaps_add_a_message)) }, - value = postViewModel.customMessage, - onValueChange = { - postViewModel.customMessage = it - }, - keyboardOptions = KeyboardOptions.Default.copy( - capitalization = KeyboardCapitalization.None, - keyboardType = KeyboardType.Text - ), - placeholder = { - Text( - text = stringResource(id = R.string.custom_zaps_add_a_message_example), - color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) - ) - }, - singleLine = true, - modifier = Modifier - .padding(end = 10.dp) - .weight(1f) - ) - } - } - } - } -} - -@Composable -fun ZapButton(isActive: Boolean, onPost: () -> Unit) { - Button( - onClick = { onPost() }, - shape = RoundedCornerShape(20.dp), - colors = ButtonDefaults - .buttonColors( - backgroundColor = if (isActive) MaterialTheme.colors.primary else Color.Gray - ) - ) { - Text(text = "⚡Zap ", color = Color.White) - } -} +package com.vitorpamplona.amethyst.ui.note + +import android.widget.Toast +import androidx.compose.foundation.layout.* +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.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +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.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.actions.CloseButton +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class ZapOptionstViewModel : ViewModel() { + private var account: Account? = null + + var customAmount by mutableStateOf(TextFieldValue("1000")) + var customMessage by mutableStateOf(TextFieldValue("")) + + fun load(account: Account) { + this.account = account + } + + fun canSend(): Boolean { + return value() != null + } + + fun value(): Long? { + return try { + customAmount.text.trim().toLongOrNull() + } catch (e: Exception) { + null + } + } + + fun cancel() { + } +} + +@Composable +fun ZapCustomDialog(onClose: () -> Unit, account: Account, accountViewModel: AccountViewModel, baseNote: Note) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + val postViewModel: ZapOptionstViewModel = viewModel() + + LaunchedEffect(account) { + postViewModel.load(account) + } + + Dialog( + onDismissRequest = { onClose() }, + properties = DialogProperties( + dismissOnClickOutside = false, + usePlatformDefaultWidth = false + ) + ) { + Surface() { + Column(modifier = Modifier.padding(10.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + CloseButton(onCancel = { + postViewModel.cancel() + onClose() + }) + + ZapButton( + isActive = postViewModel.canSend() + ) { + scope.launch(Dispatchers.IO) { + accountViewModel.zap( + baseNote, + postViewModel.value()!! * 1000L, + null, + postViewModel.customMessage.text, + context, + onError = { + scope.launch { + Toast + .makeText(context, it, Toast.LENGTH_SHORT).show() + } + }, + onProgress = { + scope.launch(Dispatchers.Main) { + } + } + ) + } + onClose() + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 5.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedTextField( + // stringResource(R.string.new_amount_in_sats + label = { Text(text = stringResource(id = R.string.amount_in_sats)) }, + value = postViewModel.customAmount, + onValueChange = { + postViewModel.customAmount = it + }, + keyboardOptions = KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.None, + keyboardType = KeyboardType.Number + ), + placeholder = { + Text( + text = "100, 1000, 5000", + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + }, + singleLine = true, + modifier = Modifier + .padding(end = 10.dp) + .weight(1f) + ) + } + Spacer(modifier = Modifier.height(5.dp)) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedTextField( + // stringResource(R.string.new_amount_in_sats + label = { Text(text = stringResource(id = R.string.custom_zaps_add_a_message)) }, + value = postViewModel.customMessage, + onValueChange = { + postViewModel.customMessage = it + }, + keyboardOptions = KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.None, + keyboardType = KeyboardType.Text + ), + placeholder = { + Text( + text = stringResource(id = R.string.custom_zaps_add_a_message_example), + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + }, + singleLine = true, + modifier = Modifier + .padding(end = 10.dp) + .weight(1f) + ) + } + } + } + } +} + +@Composable +fun ZapButton(isActive: Boolean, onPost: () -> Unit) { + Button( + onClick = { onPost() }, + shape = RoundedCornerShape(20.dp), + colors = ButtonDefaults + .buttonColors( + backgroundColor = if (isActive) MaterialTheme.colors.primary else Color.Gray + ) + ) { + Text(text = "⚡Zap ", color = Color.White) + } +} From 7e07573fa54fa0c8daa7a677411777aec682c200 Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Sat, 1 Apr 2023 10:23:45 +0900 Subject: [PATCH 039/145] fix voting on closed polls bug --- .../main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt | 2 +- .../com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 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 640fecffb6..09b8581226 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 @@ -131,7 +131,7 @@ fun ZapVote( ) .show() } - } else if (pollViewModel.isPollClosed) { + } else if (pollViewModel.isPollClosed()) { scope.launch { Toast .makeText( 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 77080d3e01..2da8418d28 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 @@ -30,8 +30,8 @@ class PollNoteViewModel { 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 + fun isPollClosed(): Boolean = closedAt?.let { // allow 2 minute leeway for zap to propagate + pollNote?.createdAt()?.plus(it * (86400 + 120))!! < Date().time / 1000 } == true fun voteAmountPlaceHolderText(sats: String): String = if (valueMinimum == null && valueMaximum == null) { From 56926259228ba2558855793d3c606e91fa87e138 Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Sun, 2 Apr 2023 13:46:24 +0900 Subject: [PATCH 040/145] show amount for atomic polls before zapping --- .../amethyst/ui/note/PollNote.kt | 65 +++++++------------ 1 file changed, 24 insertions(+), 41 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 09b8581226..91fcd56cd2 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 @@ -150,27 +150,7 @@ fun ZapVote( .show() } return@combinedClickable - } - accountViewModel.zap( - baseNote, - pollViewModel.valueMaximum!!.toLong() * 1000, - pollOption, - "", - context, - onError = { - scope.launch { - zappingProgress = 0f - Toast - .makeText(context, it, Toast.LENGTH_SHORT) - .show() - } - }, - onProgress = { - scope.launch(Dispatchers.Main) { - zappingProgress = it - } - } - ) + } else { wantsToZap = true } } else { wantsToZap = true } @@ -266,27 +246,30 @@ fun ZapVoteAmountChoicePopup( modifier = Modifier .padding(10.dp) ) { - val amount = pollViewModel.inputVoteAmountLong(inputAmountText) + var amount = pollViewModel.inputVoteAmountLong(inputAmountText) - OutlinedTextField( - value = inputAmountText, - onValueChange = { inputAmountText = it }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.width(150.dp), - colors = if (pollViewModel.isValidInputVoteAmount(amount)) colorValid else colorInValid, - label = { - Text( - text = stringResource(R.string.poll_zap_amount), - color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) - ) - }, - placeholder = { - Text( - text = pollViewModel.voteAmountPlaceHolderText(context.getString(R.string.sats)), - color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) - ) - } - ) + // only prompt for input amount if vote is not atomic + if (!pollViewModel.isVoteAmountAtomic()) { + OutlinedTextField( + value = inputAmountText, + onValueChange = { inputAmountText = it }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.width(150.dp), + colors = if (pollViewModel.isValidInputVoteAmount(amount)) colorValid else colorInValid, + label = { + Text( + text = stringResource(R.string.poll_zap_amount), + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + }, + placeholder = { + Text( + text = pollViewModel.voteAmountPlaceHolderText(context.getString(R.string.sats)), + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + } + ) + } else { amount = pollViewModel.valueMaximum?.toLong() } val isValidInputAmount = pollViewModel.isValidInputVoteAmount(amount) Button( From 9d9283e05fcdce116064b0b0e1515160e1d15c63 Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Sun, 2 Apr 2023 13:58:07 +0900 Subject: [PATCH 041/145] clean up imports, fix translatable typo --- .../amethyst/model/LocalCache.kt | 23 ------------------- .../service/NostrSingleEventDataSource.kt | 14 +---------- .../service/NostrUserProfileDataSource.kt | 9 -------- .../amethyst/ui/dal/NotificationFeedFilter.kt | 4 ++-- .../ui/note/ChatroomMessageCompose.kt | 6 ++--- .../amethyst/ui/note/NoteCompose.kt | 12 ---------- .../ui/screen/loggedIn/ProfileScreen.kt | 4 ++-- 7 files changed, 8 insertions(+), 64 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 d94924206c..952e3095c4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -4,29 +4,6 @@ import android.util.Log import androidx.lifecycle.LiveData import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -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.BookmarkListEvent -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.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.google.gson.reflect.TypeToken import com.vitorpamplona.amethyst.service.model.* import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.ui.components.BundledUpdate 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 47edf38ab6..20b3533ef0 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.EOSETime import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.JsonFilter 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 111842b390..a4ad8a5fcc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt @@ -2,15 +2,6 @@ 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.BookmarkListEvent -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.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 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 e5aba595bf..bbdc8b61ad 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 @@ -30,8 +30,8 @@ object NotificationFeedFilter : FeedFilter() { } .filter { it -> it.event !is PollNoteEvent || - it.replyTo?.any { it.author == account.userProfile() } == true || - account.userProfile() in it.directlyCiteUsers() + it.replyTo?.any { it.author == account.userProfile() } == true || + account.userProfile() in it.directlyCiteUsers() } .filter { it.event !is ReactionEvent || diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index 36e6423c5d..1301bc917c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -60,7 +60,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage -import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer +import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -283,7 +283,7 @@ fun ChatroomMessageCompose( !noteForReports.hasAnyReports() if (eventContent != null) { - TranslateableRichTextViewer( + TranslatableRichTextViewer( eventContent, canPreview, Modifier, @@ -293,7 +293,7 @@ fun ChatroomMessageCompose( navController ) } else { - TranslateableRichTextViewer( + TranslatableRichTextViewer( stringResource(R.string.could_not_decrypt_the_message), true, Modifier, 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 c4f2927e9a..4db3034353 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 @@ -51,18 +51,6 @@ import com.vitorpamplona.amethyst.model.Account 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.EventInterface -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.* import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index 88159e4d5c..7a220b9235 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -64,7 +64,7 @@ import com.vitorpamplona.amethyst.ui.components.InvoiceRequest import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage -import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer +import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog import com.vitorpamplona.amethyst.ui.dal.UserProfileBookmarksFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileConversationsFeedFilter @@ -563,7 +563,7 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account, accountViewMode Row( modifier = Modifier.padding(top = 5.dp, bottom = 5.dp) ) { - TranslateableRichTextViewer( + TranslatableRichTextViewer( content = it, canPreview = false, tags = null, From e8e696dcead63c91bb1bca037ae6e0517b6bb60f Mon Sep 17 00:00:00 2001 From: Believethehype Date: Sun, 2 Apr 2023 23:25:37 +0200 Subject: [PATCH 042/145] added message to LnZapEvent and interface --- .../com/vitorpamplona/amethyst/service/model/LnZapEvent.kt | 4 ++++ .../amethyst/service/model/LnZapEventInterface.kt | 2 ++ 2 files changed, 6 insertions(+) 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 ff42b76763..7d1300c49b 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 @@ -36,6 +36,10 @@ class LnZapEvent( null } } + override fun message(): String { + return message + } + val message = content override fun containedPost(): Event? = try { description()?.let { 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 cc95d34b32..1ebe85dcbf 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 @@ -13,4 +13,6 @@ interface LnZapEventInterface : EventInterface { fun amount(): BigDecimal? fun containedPost(): Event? + + fun message(): String } From 20aca144faf2b858f8ea1b7cdf4d6b7952e1f750 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 3 Apr 2023 14:01:00 -0400 Subject: [PATCH 043/145] Moves Login QR Button to a leading QR, not a trailing QR --- .../ui/screen/loggedOff/LoginScreen.kt | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt index 132c997d0f..844df8e4d2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt @@ -134,22 +134,24 @@ fun LoginPage( } ) } - if (dialogOpen) { - SimpleQrCodeScanner { - dialogOpen = false - if (!it.isNullOrEmpty()) { - key.value = TextFieldValue(it) - } + } + }, + leadingIcon = { + if (dialogOpen) { + SimpleQrCodeScanner { + dialogOpen = false + if (!it.isNullOrEmpty()) { + key.value = TextFieldValue(it) } } - IconButton(onClick = { dialogOpen = true }) { - Icon( - painter = painterResource(R.drawable.ic_qrcode), - null, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colors.primary - ) - } + } + IconButton(onClick = { dialogOpen = true }) { + Icon( + painter = painterResource(R.drawable.ic_qrcode), + null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colors.primary + ) } }, visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(), From 3e666df896578728fc3e21bbc97f872b731793d0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 3 Apr 2023 14:01:43 -0400 Subject: [PATCH 044/145] Adds full nip19 parsing on key decoding capabilities. --- .../java/com/vitorpamplona/amethyst/model/Hex.kt | 12 ++++++------ .../amethyst/ui/screen/AccountStateViewModel.kt | 8 ++++++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Hex.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Hex.kt index c50ff57816..7e16f77d80 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Hex.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Hex.kt @@ -38,14 +38,14 @@ fun HexKey.toDisplayHexKey(): String { } fun decodePublicKey(key: String): ByteArray { + val parsed = Nip19.uriToRoute(key) + val pubKeyParsed = parsed?.hex?.toByteArray() + return if (key.startsWith("nsec")) { Persona(privKey = key.bechToBytes()).pubKey - } else if (key.startsWith("npub")) { - key.bechToBytes() - } else if (key.startsWith("note")) { - key.bechToBytes() - } else { // if (pattern.matcher(key).matches()) { - // } else { + } else if (pubKeyParsed != null) { + pubKeyParsed + } else { Hex.decode(key) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index 0cc9cb31f1..58c9140e97 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -4,6 +4,8 @@ import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.ServiceManager import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.toByteArray +import com.vitorpamplona.amethyst.service.nip19.Nip19 import fr.acinq.secp256k1.Hex import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi @@ -39,12 +41,14 @@ class AccountStateViewModel() : ViewModel() { fun login(key: String) { val pattern = Pattern.compile(".+@.+\\.[a-z]+") + val parsed = Nip19.uriToRoute(key) + val pubKeyParsed = parsed?.hex?.toByteArray() val account = if (key.startsWith("nsec")) { Account(Persona(privKey = key.bechToBytes())) - } else if (key.startsWith("npub")) { - Account(Persona(pubKey = key.bechToBytes())) + } else if (pubKeyParsed != null) { + Account(Persona(pubKey = pubKeyParsed)) } else if (pattern.matcher(key).matches()) { // Evaluate NIP-5 Account(Persona()) From 182cf62a3ba13c31d3a5835d145e7b9dd198f8b6 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 3 Apr 2023 14:02:19 -0400 Subject: [PATCH 045/145] v0.31.4 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 3f269ebef3..61ae98107a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 33 - versionCode 113 - versionName "0.31.3" + versionCode 114 + versionName "0.31.4" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From e4705312109bc0716160d9ae0d4ab2456ae61333 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 3 Apr 2023 15:04:52 -0400 Subject: [PATCH 046/145] Adds Show QR option to Profile screen --- .../amethyst/ui/qrcode/ShowQRDialog.kt | 22 ++++++------ .../ui/screen/loggedIn/ProfileScreen.kt | 34 ++++++++++++++++--- 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/ShowQRDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/ShowQRDialog.kt index b78c33323e..30a7ba6b9a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/ShowQRDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/qrcode/ShowQRDialog.kt @@ -66,7 +66,7 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) { modifier = Modifier .fillMaxSize() .padding(horizontal = 10.dp), - verticalArrangement = Arrangement.SpaceBetween + verticalArrangement = Arrangement.SpaceAround ) { if (presenting) { Column(modifier = Modifier.fillMaxWidth()) { @@ -91,22 +91,22 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) { fontSize = 18.sp ) } - - Row( - horizontalArrangement = Arrangement.Center, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 35.dp, vertical = 10.dp) - ) { - QrCodeDrawer("nostr:${user.pubkeyNpub()}") - } } Row( horizontalArrangement = Arrangement.Center, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 30.dp, vertical = 10.dp) + .padding(horizontal = 35.dp) + ) { + QrCodeDrawer("nostr:${user.pubkeyNpub()}") + } + + Row( + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 30.dp) ) { Button( onClick = { presenting = false }, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index 88159e4d5c..faf0bdbbe1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -73,6 +73,7 @@ import com.vitorpamplona.amethyst.ui.dal.UserProfileFollowsFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileNewThreadFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileZapsFeedFilter +import com.vitorpamplona.amethyst.ui.navigation.ShowQRDialog import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.screen.FeedView @@ -450,16 +451,41 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account, accountViewMode IconButton( modifier = Modifier - .size(30.dp) + .size(25.dp) .padding(start = 5.dp), onClick = { clipboardManager.setText(AnnotatedString(user.pubkeyNpub())); } ) { Icon( imageVector = Icons.Default.ContentCopy, null, - modifier = Modifier - .padding(end = 5.dp) - .size(15.dp), + modifier = Modifier.size(15.dp), + tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + } + + var dialogOpen by remember { + mutableStateOf(false) + } + + if (dialogOpen) { + ShowQRDialog( + user, + onScan = { + dialogOpen = false + navController.navigate(it) + }, + onClose = { dialogOpen = false } + ) + } + + IconButton( + modifier = Modifier.size(25.dp), + onClick = { dialogOpen = true } + ) { + Icon( + painter = painterResource(R.drawable.ic_qrcode), + null, + modifier = Modifier.size(15.dp), tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) } From 6a85ab743dc4601f12d4e01187ba3943f06e1931 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 3 Apr 2023 20:31:25 -0400 Subject: [PATCH 047/145] Moving new pool button to NewPost --- app/build.gradle | 4 - .../amethyst/ui/actions/NewPollClosing.kt | 6 +- .../ui/actions/NewPollConsensusThreshold.kt | 6 +- .../amethyst/ui/actions/NewPollOption.kt | 54 ++- .../ui/actions/NewPollPrimaryDescription.kt | 4 +- .../ui/actions/NewPollRecipientsField.kt | 4 +- .../amethyst/ui/actions/NewPollView.kt | 4 +- .../amethyst/ui/actions/NewPollViewModel.kt | 141 ------- .../ui/actions/NewPollVoteValueRange.kt | 4 +- .../amethyst/ui/actions/NewPostView.kt | 59 ++- .../amethyst/ui/actions/NewPostViewModel.kt | 44 ++- .../amethyst/ui/buttons/NewNoteButton.kt | 47 +++ .../amethyst/ui/note/NoteCompose.kt | 11 - .../amethyst/ui/note/PollNote.kt | 355 ++++++++++++++---- .../amethyst/ui/note/PollNoteViewModel.kt | 2 +- .../amethyst/ui/screen/loggedIn/MainScreen.kt | 4 +- 16 files changed, 459 insertions(+), 290 deletions(-) delete mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollViewModel.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewNoteButton.kt diff --git a/app/build.gradle b/app/build.gradle index d09e47ec45..3f269ebef3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -31,10 +31,6 @@ android { applicationIdSuffix '.debug' versionNameSuffix '-DEBUG' resValue "string", "app_name", "@string/app_name_debug" - - lintOptions{ - disable 'MissingTranslation' - } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollClosing.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollClosing.kt index ec4b7d6310..c6d1d1f3e5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollClosing.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollClosing.kt @@ -21,10 +21,10 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.actions.NewPollViewModel +import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel @Composable -fun NewPollClosing(pollViewModel: NewPollViewModel) { +fun NewPollClosing(pollViewModel: NewPostViewModel) { var text by rememberSaveable { mutableStateOf("") } pollViewModel.isValidClosedAt.value = true @@ -75,5 +75,5 @@ fun NewPollClosing(pollViewModel: NewPollViewModel) { @Preview @Composable fun NewPollClosingPreview() { - NewPollClosing(NewPollViewModel()) + NewPollClosing(NewPostViewModel()) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollConsensusThreshold.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollConsensusThreshold.kt index 21e346f43d..d4e4d22bbb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollConsensusThreshold.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollConsensusThreshold.kt @@ -21,10 +21,10 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.actions.NewPollViewModel +import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel @Composable -fun NewPollConsensusThreshold(pollViewModel: NewPollViewModel) { +fun NewPollConsensusThreshold(pollViewModel: NewPostViewModel) { var text by rememberSaveable { mutableStateOf("") } pollViewModel.isValidConsensusThreshold.value = true @@ -75,5 +75,5 @@ fun NewPollConsensusThreshold(pollViewModel: NewPollViewModel) { @Preview @Composable fun NewPollConsensusThresholdPreview() { - NewPollConsensusThreshold(NewPollViewModel()) + NewPollConsensusThreshold(NewPostViewModel()) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollOption.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollOption.kt index c96af2a237..876134cff8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollOption.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollOption.kt @@ -1,20 +1,20 @@ package com.vitorpamplona.amethyst.ui.actions -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R @Composable -fun NewPollOption(pollViewModel: NewPollViewModel, optionIndex: Int) { +fun NewPollOption(pollViewModel: NewPostViewModel, optionIndex: Int) { val colorInValid = TextFieldDefaults.outlinedTextFieldColors( focusedBorderColor = MaterialTheme.colors.error, unfocusedBorderColor = Color.Red @@ -25,14 +25,26 @@ fun NewPollOption(pollViewModel: NewPollViewModel, optionIndex: Int) { ) Row { + val deleteIcon: @Composable (() -> Unit) = { + IconButton( + onClick = { + pollViewModel.pollOptions.remove(optionIndex) + } + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = stringResource(R.string.clear) + ) + } + } + OutlinedTextField( - modifier = Modifier - .weight(1F), + modifier = Modifier.weight(1F), value = pollViewModel.pollOptions[optionIndex] ?: "", onValueChange = { pollViewModel.pollOptions[optionIndex] = it }, label = { Text( - text = stringResource(R.string.poll_option_index).format(optionIndex), + text = stringResource(R.string.poll_option_index).format(optionIndex + 1), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) }, @@ -42,31 +54,17 @@ fun NewPollOption(pollViewModel: NewPollViewModel, optionIndex: Int) { color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) }, - colors = if (pollViewModel.pollOptions[optionIndex]?.isNotEmpty() == true) colorValid else colorInValid + keyboardOptions = KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.Sentences + ), + // colors = if (pollViewModel.pollOptions[optionIndex]?.isNotEmpty() == true) colorValid else colorInValid, + trailingIcon = if (optionIndex > 1) deleteIcon else null ) - if (optionIndex > 1) { - Button( - modifier = Modifier - .padding(start = 6.dp, top = 2.dp) - .imePadding(), - onClick = { pollViewModel.pollOptions.remove(optionIndex) }, - border = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f)), - colors = ButtonDefaults.outlinedButtonColors( - contentColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) - ) - ) { - Image( - painterResource(id = android.R.drawable.ic_delete), - contentDescription = "Remove poll option button", - modifier = Modifier.size(18.dp) - ) - } - } } } @Preview @Composable fun NewPollOptionPreview() { - NewPollOption(NewPollViewModel(), 0) + NewPollOption(NewPostViewModel(), 0) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollPrimaryDescription.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollPrimaryDescription.kt index 1084cf41c3..685eb2e8ff 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollPrimaryDescription.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollPrimaryDescription.kt @@ -17,12 +17,12 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.actions.NewPollViewModel +import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation @OptIn(ExperimentalComposeUiApi::class) @Composable -fun NewPollPrimaryDescription(pollViewModel: NewPollViewModel) { +fun NewPollPrimaryDescription(pollViewModel: NewPostViewModel) { // initialize focus reference to be able to request focus programmatically val focusRequester = remember { FocusRequester() } val keyboardController = LocalSoftwareKeyboardController.current diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollRecipientsField.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollRecipientsField.kt index c8aac5cfcd..b84ed0480d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollRecipientsField.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollRecipientsField.kt @@ -9,10 +9,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.actions.NewPollViewModel +import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel @Composable -fun NewPollRecipientsField(pollViewModel: NewPollViewModel, account: Account) { +fun NewPollRecipientsField(pollViewModel: NewPostViewModel, account: Account) { // if no recipients, add user's pubkey if (pollViewModel.zapRecipients.isEmpty()) { pollViewModel.zapRecipients.add(account.userProfile().pubkeyHex) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollView.kt index f16623cce5..12977d096a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollView.kt @@ -33,7 +33,7 @@ import kotlinx.coroutines.delay @Composable fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = null, account: Account) { - val pollViewModel: NewPollViewModel = viewModel() + val pollViewModel: NewPostViewModel = viewModel() val context = LocalContext.current @@ -84,7 +84,7 @@ fun NewPollView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n PollButton( onPost = { - pollViewModel.sendPoll() + pollViewModel.sendPost() onClose() }, isActive = pollViewModel.message.text.isNotBlank() && diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollViewModel.kt deleted file mode 100644 index 2bd10b6a20..0000000000 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollViewModel.kt +++ /dev/null @@ -1,141 +0,0 @@ -package com.vitorpamplona.amethyst.ui.actions - -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.snapshots.SnapshotStateMap -import androidx.compose.ui.text.input.TextFieldValue -import com.vitorpamplona.amethyst.model.* -import com.vitorpamplona.amethyst.service.nip19.Nip19 - -class NewPollViewModel : NewPostViewModel() { - - var zapRecipients = mutableStateListOf() - var pollOptions = newStateMapPollOptions() - var valueMaximum: Int? = null - var valueMinimum: Int? = null - var consensusThreshold: Int? = null - var closedAt: Int? = null - - var isValidRecipients = mutableStateOf(true) - var isValidvalueMaximum = mutableStateOf(true) - var isValidvalueMinimum = mutableStateOf(true) - var isValidConsensusThreshold = mutableStateOf(true) - var isValidClosedAt = mutableStateOf(true) - - override fun load(account: Account, replyingTo: Note?, quote: Note?) { - super.load(account, replyingTo, quote) - } - - override fun addUserToMentions(user: User) { - super.addUserToMentions(user) - } - - override fun addNoteToReplyTos(note: Note) { - super.addNoteToReplyTos(note) - } - - override fun tagIndex(user: User): Int { - return super.tagIndex(user) - } - - override fun tagIndex(note: Note): Int { - return super.tagIndex(note) - } - - fun sendPoll() { - // adds all references to mentions and reply tos - message.text.split('\n').forEach { paragraph: String -> - paragraph.split(' ').forEach { word: String -> - val results = parseDirtyWordForKey(word) - - if (results?.key?.type == Nip19.Type.USER) { - addUserToMentions(LocalCache.getOrCreateUser(results.key.hex)) - } else if (results?.key?.type == Nip19.Type.NOTE) { - addNoteToReplyTos(LocalCache.getOrCreateNote(results.key.hex)) - } else if (results?.key?.type == Nip19.Type.ADDRESS) { - val note = LocalCache.checkGetOrCreateAddressableNote(results.key.hex) - if (note != null) { - addNoteToReplyTos(note) - } - } - } - } - - // Tags the text in the correct order. - val newMessage = message.text.split('\n').map { paragraph: String -> - paragraph.split(' ').map { word: String -> - val results = parseDirtyWordForKey(word) - if (results?.key?.type == Nip19.Type.USER) { - val user = LocalCache.getOrCreateUser(results.key.hex) - - "#[${tagIndex(user)}]${results.restOfWord}" - } else if (results?.key?.type == Nip19.Type.NOTE) { - val note = LocalCache.getOrCreateNote(results.key.hex) - - "#[${tagIndex(note)}]${results.restOfWord}" - } else if (results?.key?.type == Nip19.Type.ADDRESS) { - val note = LocalCache.checkGetOrCreateAddressableNote(results.key.hex) - if (note != null) { - "#[${tagIndex(note)}]${results.restOfWord}" - } else { - word - } - } else { - word - } - }.joinToString(" ") - }.joinToString("\n") - - /* if (originalNote?.channel() != null) { - account?.sendChannelMessage(newMessage, originalNote!!.channel()!!.idHex, originalNote!!, mentions) - } else { - account?.sendPoll(newMessage, replyTos, mentions) - }*/ - - account?.sendPoll(newMessage, replyTos, mentions, pollOptions, valueMaximum, valueMinimum, consensusThreshold, closedAt) - - clearPollStates() - } - - override fun cancel() { - super.cancel() - - clearPollStates() - } - - override fun findUrlInMessage(): String? { - return super.findUrlInMessage() - } - - override fun removeFromReplyList(it: User) { - super.removeFromReplyList(it) - } - - override fun updateMessage(it: TextFieldValue) { - super.updateMessage(it) - } - - override fun autocompleteWithUser(item: User) { - super.autocompleteWithUser(item) - } - - // clear all states - private fun clearPollStates() { - message = TextFieldValue("") - urlPreview = null - isUploadingImage = false - mentions = null - - zapRecipients = mutableStateListOf() - pollOptions = newStateMapPollOptions() - valueMaximum = null - valueMinimum = null - consensusThreshold = null - closedAt = null - } - - private fun newStateMapPollOptions(): SnapshotStateMap { - return mutableStateMapOf(Pair(0, ""), Pair(1, "")) - } -} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollVoteValueRange.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollVoteValueRange.kt index 63a0757591..ee6a3f4fec 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollVoteValueRange.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollVoteValueRange.kt @@ -23,7 +23,7 @@ import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R @Composable -fun NewPollVoteValueRange(pollViewModel: NewPollViewModel) { +fun NewPollVoteValueRange(pollViewModel: NewPostViewModel) { var textMax by rememberSaveable { mutableStateOf("") } var textMin by rememberSaveable { mutableStateOf("") } @@ -122,5 +122,5 @@ fun NewPollVoteValueRange(pollViewModel: NewPollViewModel) { @Preview @Composable fun NewPollVoteValueRangePreview() { - NewPollVoteValueRange(NewPollViewModel()) + NewPollVoteValueRange(NewPostViewModel()) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index 100b3fabe6..22668226b7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -1,6 +1,8 @@ package com.vitorpamplona.amethyst.ui.actions import android.widget.Toast +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image import androidx.compose.foundation.border import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn @@ -104,8 +106,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n postViewModel.sendPost() onClose() }, - isActive = postViewModel.message.text.isNotBlank() && - !postViewModel.isUploadingImage + isActive = postViewModel.canPost() ) } @@ -161,6 +162,26 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content) ) + if (postViewModel.wantsPoll) { + postViewModel.pollOptions.values.forEachIndexed { index, element -> + NewPollOption(postViewModel, index) + } + + Button( + onClick = { postViewModel.pollOptions[postViewModel.pollOptions.size] = "" }, + border = BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f)), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + ) { + Image( + painterResource(id = android.R.drawable.ic_input_add), + contentDescription = "Add poll option button", + modifier = Modifier.size(18.dp) + ) + } + } + val myUrlPreview = postViewModel.urlPreview if (myUrlPreview != null) { Row(modifier = Modifier.padding(top = 5.dp)) { @@ -217,11 +238,15 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n Row(modifier = Modifier.fillMaxWidth()) { UploadFromGallery( isUploading = postViewModel.isUploadingImage, - tint = MaterialTheme.colors.primary, + tint = MaterialTheme.colors.onBackground, modifier = Modifier.padding(bottom = 10.dp) ) { postViewModel.upload(it, context) } + + AddPollButton(postViewModel.wantsPoll) { + postViewModel.wantsPoll = !postViewModel.wantsPoll + } } } } @@ -229,6 +254,34 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n } } +@Composable +private fun AddPollButton( + isPollActive: Boolean, + onClick: () -> Unit +) { + IconButton( + onClick = { + onClick() + } + ) { + if (!isPollActive) { + Icon( + painter = painterResource(R.drawable.ic_poll), + null, + modifier = Modifier.size(20.dp), + tint = Color.White + ) + } else { + Icon( + painter = painterResource(R.drawable.ic_lists), + null, + modifier = Modifier.size(20.dp), + tint = Color.White + ) + } + } +} + @Composable fun CloseButton(onCancel: () -> Unit) { Button( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index 25635f39b5..98453d5d94 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -3,8 +3,11 @@ package com.vitorpamplona.amethyst.ui.actions import android.content.Context import android.net.Uri import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel @@ -34,6 +37,21 @@ open class NewPostViewModel : ViewModel() { var userSuggestions by mutableStateOf>(emptyList()) var userSuggestionAnchor: TextRange? = null + // Polls + var wantsPoll by mutableStateOf(false) + var zapRecipients = mutableStateListOf() + var pollOptions = newStateMapPollOptions() + var valueMaximum: Int? = null + var valueMinimum: Int? = null + var consensusThreshold: Int? = null + var closedAt: Int? = null + + var isValidRecipients = mutableStateOf(true) + var isValidvalueMaximum = mutableStateOf(true) + var isValidvalueMinimum = mutableStateOf(true) + var isValidConsensusThreshold = mutableStateOf(true) + var isValidClosedAt = mutableStateOf(true) + open fun load(account: Account, replyingTo: Note?, quote: Note?) { originalNote = replyingTo replyingTo?.let { replyNote -> @@ -130,16 +148,15 @@ open class NewPostViewModel : ViewModel() { }.joinToString(" ") }.joinToString("\n") - if (originalNote?.channel() != null) { + if (wantsPoll) { + account?.sendPoll(newMessage, replyTos, mentions, pollOptions, valueMaximum, valueMinimum, consensusThreshold, closedAt) + } else if (originalNote?.channel() != null) { account?.sendChannelMessage(newMessage, originalNote!!.channel()!!.idHex, originalNote!!, mentions) } else { account?.sendPost(newMessage, replyTos, mentions) } - message = TextFieldValue("") - urlPreview = null - isUploadingImage = false - mentions = null + cancel() } fun upload(it: Uri, context: Context) { @@ -171,6 +188,14 @@ open class NewPostViewModel : ViewModel() { urlPreview = null isUploadingImage = false mentions = null + + wantsPoll = false + zapRecipients = mutableStateListOf() + pollOptions = newStateMapPollOptions() + valueMaximum = null + valueMinimum = null + consensusThreshold = null + closedAt = null } open fun findUrlInMessage(): String? { @@ -214,4 +239,13 @@ open class NewPostViewModel : ViewModel() { userSuggestions = emptyList() } } + + private fun newStateMapPollOptions(): SnapshotStateMap { + return mutableStateMapOf(Pair(0, ""), Pair(1, "")) + } + + fun canPost(): Boolean { + return message.text.isNotBlank() && !isUploadingImage && + (!wantsPoll || pollOptions.values.all { it.isNotEmpty() }) + } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewNoteButton.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewNoteButton.kt new file mode 100644 index 0000000000..ece9c89e9b --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/buttons/NewNoteButton.kt @@ -0,0 +1,47 @@ +package com.vitorpamplona.amethyst.ui.buttons + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.ui.actions.NewPostView + +@Composable +fun NewNoteButton(account: Account) { + var wantsToPost by remember { + mutableStateOf(false) + } + + if (wantsToPost) { + NewPostView({ wantsToPost = false }, account = account) + } + + OutlinedButton( + onClick = { wantsToPost = true }, + modifier = Modifier.size(55.dp), + shape = CircleShape, + colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.primary), + contentPadding = PaddingValues(0.dp) + ) { + Icon( + painter = painterResource(R.drawable.ic_compose), + null, + modifier = Modifier.size(26.dp), + tint = Color.White + ) + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 4db3034353..d3639dded3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -524,17 +524,6 @@ fun NoteComposeInner( DisplayUncitedHashtags(noteEvent, eventContent, navController) } -/* - TranslateableRichTextViewer( - eventContent, - canPreview = canPreview && !makeItShort, - Modifier.fillMaxWidth(), - noteEvent.tags(), - backgroundColor, - accountViewModel, - navController - ) -*/ if (noteEvent is PollNoteEvent) { PollNote( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt index 91fcd56cd2..123ec0b7a4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt @@ -16,16 +16,20 @@ import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties +import androidx.compose.ui.window.Popup import androidx.navigation.NavController import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note @@ -35,55 +39,119 @@ import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.util.* +import kotlin.math.roundToInt @Composable fun PollNote( - note: Note, + baseNote: Note, canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, navController: NavController ) { + val accountState by accountViewModel.accountLiveData.observeAsState() + val account = accountState?.account ?: return + + val zapsState by baseNote.live().zaps.observeAsState() + val zappedNote = zapsState?.note ?: return + val pollViewModel = PollNoteViewModel() - pollViewModel.load(note) + pollViewModel.load(zappedNote) pollViewModel.pollEvent?.pollOptions()?.forEach { poll_op -> val optionTally = pollViewModel.optionVoteTally(poll_op.key) - - Row( - verticalAlignment = Alignment.CenterVertically + val color = if ( + pollViewModel.consensusThreshold != null && + optionTally >= pollViewModel.consensusThreshold!! ) { - TranslatableRichTextViewer( - poll_op.value, - canPreview, - modifier = Modifier - .width(250.dp) - .padding(0.dp) // padding outside border - .border(BorderStroke(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.32f))) - .padding(4.dp), // padding between border and text - pollViewModel.pollEvent?.tags(), - backgroundColor, - accountViewModel, - navController - ) - - ZapVote(note, accountViewModel, pollViewModel, poll_op.key) + Color.Green.copy(alpha = 0.32f) + } else { + MaterialTheme.colors.primary.copy(alpha = 0.32f) } Row( - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(vertical = 3.dp) ) { - // only show tallies after user has zapped note - if (note.isZappedBy(accountViewModel.userProfile())) { - LinearProgressIndicator( - modifier = Modifier.width(250.dp), - color = if ( - pollViewModel.consensusThreshold != null && - optionTally >= pollViewModel.consensusThreshold!! - ) { - Color.Green - } else { MaterialTheme.colors.primary }, - progress = optionTally + if (zappedNote.author == account.userProfile() || zappedNote.isZappedBy(account.userProfile()) == true) { + ZapVote( + baseNote, + accountViewModel, + pollViewModel, + poll_op.key, + nonClickablePrepend = { + Box( + Modifier.fillMaxWidth(0.75f).clip(shape = RoundedCornerShape(15.dp)) + .border( + 2.dp, + color, + RoundedCornerShape(15.dp) + ) + ) { + LinearProgressIndicator( + modifier = Modifier.matchParentSize(), + color = color, + progress = optionTally + ) + + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Column( + horizontalAlignment = Alignment.End, + modifier = Modifier.padding(horizontal = 10.dp).width(40.dp) + ) { + Text( + text = "${(optionTally * 100).roundToInt()}%", + fontWeight = FontWeight.Bold + ) + } + + Column(modifier = Modifier.fillMaxWidth()) { + TranslatableRichTextViewer( + poll_op.value, + canPreview, + Modifier.padding(vertical = 5.dp), + pollViewModel.pollEvent?.tags(), + backgroundColor, + accountViewModel, + navController + ) + } + } + } + }, + clickablePrepend = { + } + ) + } else { + ZapVote( + baseNote, + accountViewModel, + pollViewModel, + poll_op.key, + nonClickablePrepend = {}, + clickablePrepend = { + Box( + Modifier.fillMaxWidth(0.75f) + .clip(shape = RoundedCornerShape(15.dp)) + .border( + 2.dp, + MaterialTheme.colors.primary, + RoundedCornerShape(15.dp) + ) + ) { + TranslatableRichTextViewer( + poll_op.value, + canPreview, + Modifier.padding(vertical = 5.dp, horizontal = 10.dp), + pollViewModel.pollEvent?.tags(), + backgroundColor, + accountViewModel, + navController + ) + } + } ) } } @@ -97,11 +165,10 @@ fun ZapVote( accountViewModel: AccountViewModel, pollViewModel: PollNoteViewModel, pollOption: Int, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + nonClickablePrepend: @Composable () -> Unit, + clickablePrepend: @Composable () -> Unit ) { - val accountState by accountViewModel.accountLiveData.observeAsState() - val account = accountState?.account ?: return - val zapsState by baseNote.live().zaps.observeAsState() val zappedNote = zapsState?.note @@ -112,63 +179,95 @@ fun ZapVote( var zappingProgress by remember { mutableStateOf(0f) } + val accountState by accountViewModel.accountLiveData.observeAsState() + val account = accountState?.account ?: return + + nonClickablePrepend() + Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .then(Modifier.size(20.dp)) - .combinedClickable( - role = Role.Button, - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false, radius = 24.dp), - onClick = { - if (!accountViewModel.isWriteable()) { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.login_with_a_private_key_to_be_able_to_send_zaps), - Toast.LENGTH_SHORT - ) - .show() - } - } else if (pollViewModel.isPollClosed()) { - scope.launch { - Toast - .makeText( - context, - context.getString(R.string.poll_is_closed), - Toast.LENGTH_SHORT - ) - .show() - } - } else if (pollViewModel.isVoteAmountAtomic()) { - // only allow one vote per option when min==max, i.e. atomic vote amount specified - if (pollViewModel.isPollOptionZappedBy(pollOption, account.userProfile())) { - scope.launch { - Toast - .makeText(context, R.string.one_vote_per_user_on_atomic_votes, Toast.LENGTH_SHORT) - .show() - } - return@combinedClickable - } else { wantsToZap = true } - } else { - wantsToZap = true + modifier = Modifier.combinedClickable( + role = Role.Button, + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false, radius = 24.dp), + onClick = { + if (!accountViewModel.isWriteable()) { + scope.launch { + Toast + .makeText( + context, + context.getString(R.string.login_with_a_private_key_to_be_able_to_send_zaps), + Toast.LENGTH_SHORT + ) + .show() } - }, - onLongClick = {} - ) + } else if (pollViewModel.isPollClosed()) { + scope.launch { + Toast + .makeText( + context, + context.getString(R.string.poll_is_closed), + Toast.LENGTH_SHORT + ) + .show() + } + } else if (pollViewModel.isVoteAmountAtomic() && pollViewModel.isPollOptionZappedBy(pollOption, accountViewModel.userProfile())) { + // only allow one vote per option when min==max, i.e. atomic vote amount specified + scope.launch { + Toast + .makeText( + context, + R.string.one_vote_per_user_on_atomic_votes, + Toast.LENGTH_SHORT + ) + .show() + } + return@combinedClickable + } else if (account.zapAmountChoices.size == 1 && pollViewModel.isValidInputVoteAmount(account.zapAmountChoices.first())) { + scope.launch(Dispatchers.IO) { + accountViewModel.zap( + baseNote, + account.zapAmountChoices.first() * 1000, + pollOption, + "", + context, + onError = { + scope.launch { + zappingProgress = 0f + Toast + .makeText(context, it, Toast.LENGTH_SHORT) + .show() + } + }, + onProgress = { + scope.launch(Dispatchers.Main) { + zappingProgress = it + } + } + ) + } + } else { + wantsToZap = true + } + } + ) ) { if (wantsToZap) { - ZapVoteAmountChoicePopup( + FilteredZapAmountChoicePopup( baseNote, accountViewModel, pollViewModel, pollOption, onDismiss = { wantsToZap = false + zappingProgress = 0f + }, + onChangeAmount = { + wantsToZap = false }, onError = { scope.launch { + zappingProgress = 0f Toast.makeText(context, it, Toast.LENGTH_SHORT).show() } }, @@ -180,7 +279,9 @@ fun ZapVote( ) } - if (pollViewModel.isPollOptionZappedBy(pollOption, account.userProfile())) { + clickablePrepend() + + if (pollViewModel.isPollOptionZappedBy(pollOption, accountViewModel.userProfile())) { Icon( imageVector = Icons.Default.Bolt, contentDescription = stringResource(R.string.zaps), @@ -198,7 +299,7 @@ fun ZapVote( } // only show tallies after a user has zapped note - if (zappedNote?.isZappedBy(account.userProfile()) == true) { + if (baseNote.author == accountViewModel.userProfile() || zappedNote?.isZappedBy(accountViewModel.userProfile()) == true) { Text( showAmount(pollViewModel.zappedPollOptionAmount(pollOption)), fontSize = 14.sp, @@ -208,6 +309,98 @@ fun ZapVote( } } +@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class) +@Composable +fun FilteredZapAmountChoicePopup( + baseNote: Note, + accountViewModel: AccountViewModel, + pollViewModel: PollNoteViewModel, + pollOption: Int, + onDismiss: () -> Unit, + onChangeAmount: () -> Unit, + onError: (text: String) -> Unit, + onProgress: (percent: Float) -> Unit +) { + val context = LocalContext.current + + val accountState by accountViewModel.accountLiveData.observeAsState() + val account = accountState?.account ?: return + val zapMessage = "" + val scope = rememberCoroutineScope() + + val options = account.zapAmountChoices.filter { pollViewModel.isValidInputVoteAmount(it) }.toMutableList() + if (options.isEmpty()) { + pollViewModel.valueMinimum?.let { minimum -> + pollViewModel.valueMaximum?.let { maximum -> + if (minimum != maximum) { + options.add(((minimum + maximum) / 2).toLong()) + } + } + } + } + pollViewModel.valueMinimum?.let { options.add(it.toLong()) } + pollViewModel.valueMaximum?.let { options.add(it.toLong()) } + val sortedOptions = options.toSet().sorted() + + Popup( + alignment = Alignment.BottomCenter, + offset = IntOffset(0, -100), + onDismissRequest = { onDismiss() } + ) { + FlowRow(horizontalArrangement = Arrangement.Center) { + sortedOptions.forEach { amountInSats -> + Button( + modifier = Modifier.padding(horizontal = 3.dp), + onClick = { + scope.launch(Dispatchers.IO) { + accountViewModel.zap( + baseNote, + amountInSats * 1000, + pollOption, + zapMessage, + context, + onError, + onProgress + ) + onDismiss() + } + }, + shape = RoundedCornerShape(20.dp), + colors = ButtonDefaults + .buttonColors( + backgroundColor = MaterialTheme.colors.primary + ) + ) { + Text( + "⚡ ${showAmount(amountInSats.toBigDecimal().setScale(1))}", + color = Color.White, + textAlign = TextAlign.Center, + modifier = Modifier.combinedClickable( + onClick = { + scope.launch(Dispatchers.IO) { + accountViewModel.zap( + baseNote, + amountInSats * 1000, + pollOption, + zapMessage, + context, + onError, + onProgress + ) + onDismiss() + } + }, + onLongClick = { + onChangeAmount() + } + ) + ) + } + } + } + } +} + @OptIn(ExperimentalFoundationApi::class) @Composable fun ZapVoteAmountChoicePopup( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt index 2da8418d28..c967261c02 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt @@ -14,7 +14,7 @@ class PollNoteViewModel { var pollEvent: PollNoteEvent? = null private var pollOptions: Map? = null var valueMaximum: Int? = null - private var valueMinimum: Int? = null + var valueMinimum: Int? = null private var closedAt: Int? = null var consensusThreshold: Float? = null diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt index df2c14bc58..e0293b5d25 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt @@ -23,7 +23,7 @@ import androidx.compose.ui.Modifier import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.vitorpamplona.amethyst.buttons.NewChannelButton -import com.vitorpamplona.amethyst.ui.buttons.FabColumn +import com.vitorpamplona.amethyst.ui.buttons.NewNoteButton import com.vitorpamplona.amethyst.ui.navigation.* import com.vitorpamplona.amethyst.ui.navigation.AccountSwitchBottomSheet import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar @@ -91,7 +91,7 @@ fun FloatingButtons(navController: NavHostController, accountViewModel: AccountS // Does nothing. } is AccountState.LoggedIn -> { - FabColumn(state.account) + NewNoteButton(state.account) } } } From a11263ce9e1ffef6b9b7237eca3322b7e6b2ba29 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 3 Apr 2023 20:42:05 -0400 Subject: [PATCH 048/145] Correcting format --- .../java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 69d9c7871f..1b04d958a9 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 @@ -67,4 +67,4 @@ class LnZapEvent( companion object { const val kind = 9735 } -} \ No newline at end of file +} From e96e218025c412b3646809234d3c3015e4c2f168 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 3 Apr 2023 20:46:22 -0400 Subject: [PATCH 049/145] Adds relay param in spam filter for polls --- .../main/java/com/vitorpamplona/amethyst/model/LocalCache.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e08e8ac8ab..d13b3c7d61 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -232,7 +232,7 @@ object LocalCache { // Already processed this event. if (note.event != null) return - if (antiSpam.isSpam(event)) { + if (antiSpam.isSpam(event, relay)) { relay?.let { it.spamCounter++ } From 15822fee8ea3cc01e4ca2deba8a2b427ae058ee2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 4 Apr 2023 11:20:57 -0400 Subject: [PATCH 050/145] Fixes Translation bug not being active on google play edition --- .../amethyst/ui/components/TranslatableRichTextViewer.kt | 0 app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt | 2 +- ...slateableRichTextViewer.kt => TranslatableRichTextViewer.kt} | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename app/src/{main => fdroid}/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt (100%) rename app/src/play/java/com/vitorpamplona/amethyst/ui/components/{TranslateableRichTextViewer.kt => TranslatableRichTextViewer.kt} (99%) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt b/app/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt similarity index 100% rename from app/src/main/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt rename to app/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt 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 1d7d3b1de0..2accfc0167 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -397,7 +397,7 @@ class Account( consensusThreshold = consensusThreshold, closedAt = closedAt ) - println("Sending new PollNoteEvent: %s".format(signedEvent.toJson())) + // println("Sending new PollNoteEvent: %s".format(signedEvent.toJson())) Client.send(signedEvent) LocalCache.consume(signedEvent) } diff --git a/app/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslateableRichTextViewer.kt b/app/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt similarity index 99% rename from app/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslateableRichTextViewer.kt rename to app/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt index 0ab0016d14..211aa00af7 100644 --- a/app/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslateableRichTextViewer.kt +++ b/app/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt @@ -38,7 +38,7 @@ import kotlinx.coroutines.withContext import java.util.Locale @Composable -fun TranslateableRichTextViewer( +fun TranslatableRichTextViewer( content: String, canPreview: Boolean, modifier: Modifier = Modifier, From b685b3856535b04fab2ba003aff0aad1be60ea91 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 4 Apr 2023 11:21:36 -0400 Subject: [PATCH 051/145] Avoids triggering exceptions when the NIP19 parser receives a simple hex key --- .../java/com/vitorpamplona/amethyst/service/nip19/Nip19.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Nip19.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Nip19.kt index b4bdda2039..87be07093c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Nip19.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Nip19.kt @@ -19,7 +19,10 @@ object Nip19 { try { val matcher = nip19regex.matcher(uri) - matcher.find() + if (!matcher.find()) { + return null + } + val uriScheme = matcher.group(1) // nostr: val type = matcher.group(2) // npub1 val key = matcher.group(3) // bech32 From e10c5771d8d17de888ec36c4ad055457bb9d0087 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 4 Apr 2023 11:27:23 -0400 Subject: [PATCH 052/145] v0.32.0 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 61ae98107a..ad2393d578 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 33 - versionCode 114 - versionName "0.31.4" + versionCode 115 + versionName "0.32.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From efd2ddba7196748e7fe2e8af80e149216264d6b6 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 4 Apr 2023 12:25:32 -0400 Subject: [PATCH 053/145] Adds payment animation to Polls --- .../amethyst/ui/note/PollNote.kt | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 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 123ec0b7a4..f68efbe7a8 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 @@ -282,6 +282,7 @@ fun ZapVote( clickablePrepend() if (pollViewModel.isPollOptionZappedBy(pollOption, accountViewModel.userProfile())) { + zappingProgress = 1f Icon( imageVector = Icons.Default.Bolt, contentDescription = stringResource(R.string.zaps), @@ -289,12 +290,21 @@ fun ZapVote( 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) - ) + if (zappingProgress < 0.1 || zappingProgress > 0.99) { + Icon( + imageVector = Icons.Outlined.Bolt, + contentDescription = stringResource(id = R.string.zaps), + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + } else { + Spacer(Modifier.width(3.dp)) + CircularProgressIndicator( + progress = zappingProgress, + modifier = Modifier.size(14.dp), + strokeWidth = 2.dp + ) + } } } From 02a9aaab601c84c6ffaa20f057bdbcf584b317a3 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 4 Apr 2023 12:29:25 -0400 Subject: [PATCH 054/145] Removes the 5dp space on the generic RichText Viewer --- .../amethyst/ui/components/TranslatableRichTextViewer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt b/app/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt index 211aa00af7..8100a3e9b1 100644 --- a/app/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt +++ b/app/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt @@ -79,7 +79,7 @@ fun TranslatableRichTextViewer( val toBeViewed = if (showOriginal) content else translatedTextState.value.result ?: content - Column(modifier = Modifier.padding(top = 5.dp)) { + Column() { ExpandableRichTextViewer( toBeViewed, canPreview, From a68ea1abe5e17e3b9bb757b00f968a8a6e975ce9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 4 Apr 2023 12:35:42 -0400 Subject: [PATCH 055/145] Adds margin between the reply note and the text --- .../main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt | 2 ++ 1 file changed, 2 insertions(+) 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 f121f1d9df..5ea90982a8 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 @@ -371,6 +371,7 @@ fun NoteComposeInner( } else { ReplyInformation(note.replyTo, sortedMentions, account, navController) } + Spacer(modifier = Modifier.height(5.dp)) } else if (!makeItShort && noteEvent is ChannelMessageEvent && (note.replyTo != null || noteEvent.mentions() != null)) { val sortedMentions = noteEvent.mentions() .mapNotNull { LocalCache.checkGetOrCreateUser(it) } @@ -380,6 +381,7 @@ fun NoteComposeInner( note.channel()?.let { ReplyInformationChannel(note.replyTo, sortedMentions, it, navController) } + Spacer(modifier = Modifier.height(5.dp)) } if (noteEvent is ReactionEvent || noteEvent is RepostEvent) { From b09bb0c3c5b5bd1efd04cafa69f2179abd3a33d0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 4 Apr 2023 13:39:12 -0400 Subject: [PATCH 056/145] Fixes margins when images are used in the poll --- .../vitorpamplona/amethyst/ui/components/ZoomableImageView.kt | 1 - .../main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt | 2 +- .../com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt | 4 +++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableImageView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableImageView.kt index 19b3b2a245..e547da6bd5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableImageView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableImageView.kt @@ -69,7 +69,6 @@ fun ZoomableImageView(word: String, images: List = listOf(word)) { contentDescription = word, contentScale = ContentScale.FillWidth, modifier = Modifier - .padding(top = 4.dp) .fillMaxWidth() .clip(shape = RoundedCornerShape(15.dp)) .border( 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 f68efbe7a8..d84e3b68fe 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 @@ -107,7 +107,7 @@ fun PollNote( ) } - Column(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.fillMaxWidth().padding(10.dp)) { TranslatableRichTextViewer( poll_op.value, canPreview, 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 f1c38eaaab..8ac5fe9e3e 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 @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn @@ -296,8 +297,9 @@ fun NoteMaster( } } + Spacer(modifier = Modifier.height(10.dp)) + if (noteEvent is BadgeDefinitionEvent) { - Spacer(modifier = Modifier.padding(top = 10.dp)) BadgeDisplay(baseNote = note) } else if (noteEvent is LongTextNoteEvent) { Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 10.dp)) { From ca2c811859f5bbee76766b0c02fbb668cac4014f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 4 Apr 2023 13:49:41 -0400 Subject: [PATCH 057/145] Fixes Voted option highlight. --- .../java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt | 2 ++ .../vitorpamplona/amethyst/service/model/LnZapEventInterface.kt | 2 ++ .../com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) 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 1b04d958a9..ad83690e91 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 @@ -27,6 +27,8 @@ class LnZapEvent( .filter { it.firstOrNull() == "p" } .mapNotNull { it.getOrNull(1) } + override fun zappedRequestAuthor(): String? = containedPost()?.pubKey() + override fun amount(): BigDecimal? { return amount } 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 c31d9658aa..b69e027817 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 @@ -10,6 +10,8 @@ interface LnZapEventInterface : EventInterface { fun zappedAuthor(): List + fun zappedRequestAuthor(): String? + fun taggedAddresses(): List fun amount(): BigDecimal? 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 c967261c02..87cbe6fa0d 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 @@ -94,7 +94,7 @@ class PollNoteViewModel { .filterIsInstance() .map { val zappedOption = it.zappedPollOption() - if (zappedOption == option) { + if (zappedOption == option && it.zappedRequestAuthor() == user.pubkeyHex ) { return true } } From a3b9ea6a42ba09b2953e2d7ee019998b0747ed1a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 4 Apr 2023 13:51:27 -0400 Subject: [PATCH 058/145] Fixes format --- .../com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 87cbe6fa0d..e5d3d4cd84 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 @@ -94,7 +94,7 @@ class PollNoteViewModel { .filterIsInstance() .map { val zappedOption = it.zappedPollOption() - if (zappedOption == option && it.zappedRequestAuthor() == user.pubkeyHex ) { + if (zappedOption == option && it.zappedRequestAuthor() == user.pubkeyHex) { return true } } From 7ce2a0bbeaf468fe1762f0841449e5b855566805 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 4 Apr 2023 13:57:06 -0400 Subject: [PATCH 059/145] Makes sure before and after voting the padding is the same. --- .../java/com/vitorpamplona/amethyst/ui/note/PollNote.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 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 d84e3b68fe..b39b1aea27 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 @@ -107,11 +107,11 @@ fun PollNote( ) } - Column(modifier = Modifier.fillMaxWidth().padding(10.dp)) { + Column(modifier = Modifier.fillMaxWidth().padding(15.dp)) { TranslatableRichTextViewer( poll_op.value, canPreview, - Modifier.padding(vertical = 5.dp), + Modifier, pollViewModel.pollEvent?.tags(), backgroundColor, accountViewModel, @@ -144,7 +144,7 @@ fun PollNote( TranslatableRichTextViewer( poll_op.value, canPreview, - Modifier.padding(vertical = 5.dp, horizontal = 10.dp), + Modifier.padding(15.dp), pollViewModel.pollEvent?.tags(), backgroundColor, accountViewModel, From 875d9544ecc9ff25a1ae6c9804f698af4853ac50 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 4 Apr 2023 13:59:02 -0400 Subject: [PATCH 060/145] Fixes Poll Color for Light mode. --- .../java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index 68102e471e..1d5343439a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -269,14 +269,14 @@ private fun AddPollButton( painter = painterResource(R.drawable.ic_poll), null, modifier = Modifier.size(20.dp), - tint = Color.White + tint = MaterialTheme.colors.onBackground ) } else { Icon( painter = painterResource(R.drawable.ic_lists), null, modifier = Modifier.size(20.dp), - tint = Color.White + tint = MaterialTheme.colors.onBackground ) } } From 83bcf5358324a5ab0732ba9eaa6db895a520bdfb Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 4 Apr 2023 14:11:05 -0400 Subject: [PATCH 061/145] v0.32.1 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ad2393d578..cc536b0687 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 33 - versionCode 115 - versionName "0.32.0" + versionCode 116 + versionName "0.32.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From a8a3915c8d7ad27b597d0bf018f97a79b44bd6bf Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 4 Apr 2023 16:09:54 -0400 Subject: [PATCH 062/145] Allowing replies of PMs from Notifications. --- app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt | 4 ++-- .../com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt | 3 +++ .../amethyst/ui/screen/loggedIn/ChatroomScreen.kt | 2 +- 3 files changed, 6 insertions(+), 3 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 2accfc0167..442688933b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -419,12 +419,12 @@ class Account( LocalCache.consume(signedEvent, null) } - fun sendPrivateMessage(message: String, toUser: String, replyingTo: Note? = null) { + fun sendPrivateMessage(message: String, toUser: String, replyingTo: Note? = null, mentions: List?) { if (!isWriteable()) return val user = LocalCache.users[toUser] ?: return val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null } - val mentionsHex = emptyList() + val mentionsHex = mentions?.map { it.pubkeyHex } val signedEvent = PrivateDmEvent.create( recipientPubKey = user.pubkey(), 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 98453d5d94..c4edd61943 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 @@ -13,6 +13,7 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.* +import com.vitorpamplona.amethyst.service.model.PrivateDmEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.service.nip19.Nip19 import com.vitorpamplona.amethyst.ui.components.isValidURL @@ -152,6 +153,8 @@ open class NewPostViewModel : ViewModel() { account?.sendPoll(newMessage, replyTos, mentions, pollOptions, valueMaximum, valueMinimum, consensusThreshold, closedAt) } else if (originalNote?.channel() != null) { account?.sendChannelMessage(newMessage, originalNote!!.channel()!!.idHex, originalNote!!, mentions) + } else if (originalNote?.event is PrivateDmEvent) { + account?.sendPrivateMessage(newMessage, originalNote!!.author!!.pubkeyHex, originalNote!!, mentions) } else { account?.sendPost(newMessage, replyTos, mentions) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt index e03561acc4..0f26c18bcd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt @@ -177,7 +177,7 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr trailingIcon = { PostButton( onPost = { - account.sendPrivateMessage(chatRoomScreenModel.message.text, userId, replyTo.value) + account.sendPrivateMessage(chatRoomScreenModel.message.text, userId, replyTo.value, null) chatRoomScreenModel.message = TextFieldValue("") replyTo.value = null feedViewModel.invalidateData() // Don't wait a full second before updating From 83d7085375da815f5a7fbd293c071ec109943653 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 4 Apr 2023 16:10:55 -0400 Subject: [PATCH 063/145] Hides Poll option when replying to channel or DMs. --- .../com/vitorpamplona/amethyst/ui/actions/NewPostView.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index 1d5343439a..fba0b3fa13 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -244,8 +244,10 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n postViewModel.upload(it, context) } - AddPollButton(postViewModel.wantsPoll) { - postViewModel.wantsPoll = !postViewModel.wantsPoll + if (postViewModel.canUsePoll()) { + AddPollButton(postViewModel.wantsPoll) { + postViewModel.wantsPoll = !postViewModel.wantsPoll + } } } } From 05138232f4fe582bdb8006072bd89b67396a4145 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 4 Apr 2023 16:11:22 -0400 Subject: [PATCH 064/145] Fixes missing function --- .../com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt | 4 ++++ .../vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) 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 c4edd61943..21f4799316 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 @@ -251,4 +251,8 @@ open class NewPostViewModel : ViewModel() { return message.text.isNotBlank() && !isUploadingImage && (!wantsPoll || pollOptions.values.all { it.isNotEmpty() }) } + + fun canUsePoll(): Boolean { + return originalNote?.event !is PrivateDmEvent && originalNote?.channel() == null + } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index 1301bc917c..6c5f6a6338 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -286,7 +286,7 @@ fun ChatroomMessageCompose( TranslatableRichTextViewer( eventContent, canPreview, - Modifier, + Modifier.padding(top = 5.dp), note.event?.tags(), backgroundBubbleColor, accountViewModel, @@ -296,7 +296,7 @@ fun ChatroomMessageCompose( TranslatableRichTextViewer( stringResource(R.string.could_not_decrypt_the_message), true, - Modifier, + Modifier.padding(top = 5.dp), note.event?.tags(), backgroundBubbleColor, accountViewModel, From 9944f98bdef1906ff2d10d3ae4062240bbda00ab Mon Sep 17 00:00:00 2001 From: Braydon Date: Tue, 4 Apr 2023 16:24:02 -0700 Subject: [PATCH 065/145] Update readme gradlew install commands --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bde5d6e5ca..7ac9ac985d 100644 --- a/README.md +++ b/README.md @@ -106,8 +106,15 @@ Build the app: ``` ## Installing on device + +For the F-Droid build: ```bash -./gradlew installDebug +./gradlew installFdroidDebug +``` + +For the Play build: +```bash +./gradlew installPlayDebug ``` ## How to Deploy From d37fef94fe4e6cb9580125ba87520458c4c6623f Mon Sep 17 00:00:00 2001 From: Poussinou Date: Wed, 5 Apr 2023 21:24:47 +0930 Subject: [PATCH 066/145] Update README.md --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 7ac9ac985d..795acbae60 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,15 @@ Amethyst brings the best social network to your Android phone. Just insert your Nostr private key and start posting. +[Get it on F-Droid](https://f-droid.org/packages/com.vitorpamplona.amethyst/) +[Get it on Google Play](https://play.google.com/store/apps/details?id=com.vitorpamplona.amethyst) + +Or get the latest APK from the [Releases Section](https://github.com/vitorpamplona/amethyst/releases/latest). + # Current Features - [x] Event Builders / WebSocket Subscriptions (NIP-01, NIP-15) From 924b21cdfcb6d9af65adc95325a185cedbc482e9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 5 Apr 2023 08:44:48 -0400 Subject: [PATCH 067/145] Reducing the use of Slow disk access of Local Preferences. --- .../amethyst/LocalPreferences.kt | 102 ++++++------ .../ui/navigation/AccountSwitchBottomSheet.kt | 149 ++++++++++-------- .../ui/screen/AccountStateViewModel.kt | 17 +- .../ui/screen/loggedOff/LoginScreen.kt | 4 +- 4 files changed, 143 insertions(+), 129 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 1726474863..c2c21bb244 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -27,10 +27,7 @@ private const val DEBUG_PREFERENCES_NAME = "debug_prefs" data class AccountInfo( val npub: String, - val hasPrivKey: Boolean, - val current: Boolean, - val displayName: String?, - val profilePicture: String? + val hasPrivKey: Boolean = false ) private object PrefKeys { @@ -38,8 +35,6 @@ private object PrefKeys { const val SAVED_ACCOUNTS = "all_saved_accounts" const val NOSTR_PRIVKEY = "nostr_privkey" const val NOSTR_PUBKEY = "nostr_pubkey" - const val DISPLAY_NAME = "display_name" - const val PROFILE_PICTURE_URL = "profile_picture" const val FOLLOWING_CHANNELS = "following_channels" const val HIDDEN_USERS = "hidden_users" const val RELAYS = "relays" @@ -59,53 +54,73 @@ private val gson = GsonBuilder().create() object LocalPreferences { private const val comma = "," - private var currentAccount: String? - get() = encryptedPreferences().getString(PrefKeys.CURRENT_ACCOUNT, null) - set(npub) { - val prefs = encryptedPreferences() - prefs.edit().apply { + private var _currentAccount: String? = null + + private fun currentAccount(): String? { + if (_currentAccount == null) { + _currentAccount = encryptedPreferences().getString(PrefKeys.CURRENT_ACCOUNT, null) + } + return _currentAccount + } + + private fun updateCurrentAccount(npub: String) { + if (_currentAccount != npub) { + _currentAccount = npub + + encryptedPreferences().edit().apply { putString(PrefKeys.CURRENT_ACCOUNT, npub) }.apply() } + } - private val savedAccounts: List - get() = encryptedPreferences() - .getString(PrefKeys.SAVED_ACCOUNTS, null)?.split(comma) ?: listOf() + private var _savedAccounts: List? = null + + private fun savedAccounts(): List { + if (_savedAccounts == null) { + _savedAccounts = encryptedPreferences() + .getString(PrefKeys.SAVED_ACCOUNTS, null)?.split(comma) ?: listOf() + } + return _savedAccounts!! + } + + private fun updateSavedAccounts(accounts: List) { + if (_savedAccounts != accounts) { + _savedAccounts = accounts + + encryptedPreferences().edit().apply { + putString(PrefKeys.SAVED_ACCOUNTS, accounts.joinToString(comma).ifBlank { null }) + }.apply() + } + } private val prefsDirPath: String get() = "${Amethyst.instance.filesDir.parent}/shared_prefs/" private fun addAccount(npub: String) { - val accounts = savedAccounts.toMutableList() + val accounts = savedAccounts().toMutableList() if (npub !in accounts) { accounts.add(npub) + updateSavedAccounts(accounts) } - val prefs = encryptedPreferences() - prefs.edit().apply { - putString(PrefKeys.SAVED_ACCOUNTS, accounts.joinToString(comma).ifBlank { null }) - }.apply() } private fun setCurrentAccount(account: Account) { val npub = account.userProfile().pubkeyNpub() - currentAccount = npub + updateCurrentAccount(npub) addAccount(npub) } fun switchToAccount(npub: String) { - currentAccount = npub + updateCurrentAccount(npub) } /** * Removes the account from the app level shared preferences */ private fun removeAccount(npub: String) { - val accounts = savedAccounts.toMutableList() + val accounts = savedAccounts().toMutableList() if (accounts.remove(npub)) { - val prefs = encryptedPreferences() - prefs.edit().apply { - putString(PrefKeys.SAVED_ACCOUNTS, accounts.joinToString(comma).ifBlank { null }) - }.apply() + updateSavedAccounts(accounts) } } @@ -145,11 +160,11 @@ object LocalPreferences { removeAccount(npub) deleteUserPreferenceFile(npub) - if (savedAccounts.isEmpty()) { + if (savedAccounts().isEmpty()) { val appPrefs = encryptedPreferences() appPrefs.edit().clear().apply() - } else if (currentAccount == npub) { - currentAccount = savedAccounts.elementAt(0) + } else if (currentAccount() == npub) { + updateCurrentAccount(savedAccounts().elementAt(0)) } } @@ -159,17 +174,8 @@ object LocalPreferences { } fun allSavedAccounts(): List { - return savedAccounts.map { npub -> - val prefs = encryptedPreferences(npub) - val hasPrivKey = prefs.getString(PrefKeys.NOSTR_PRIVKEY, null) != null - - AccountInfo( - npub = npub, - hasPrivKey = hasPrivKey, - current = npub == currentAccount, - displayName = prefs.getString(PrefKeys.DISPLAY_NAME, null), - profilePicture = prefs.getString(PrefKeys.PROFILE_PICTURE_URL, null) - ) + return savedAccounts().map { npub -> + AccountInfo(npub = npub) } } @@ -189,13 +195,11 @@ object LocalPreferences { putString(PrefKeys.LATEST_CONTACT_LIST, Event.gson.toJson(account.backupContactList)) putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog) putBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, account.hideBlockAlertDialog) - putString(PrefKeys.DISPLAY_NAME, account.userProfile().toBestDisplayName()) - putString(PrefKeys.PROFILE_PICTURE_URL, account.userProfile().profilePicture()) }.apply() } fun loadFromEncryptedStorage(): Account? { - encryptedPreferences(currentAccount).apply { + encryptedPreferences(currentAccount()).apply { val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) ?: return null val privKey = getString(PrefKeys.NOSTR_PRIVKEY, null) val followingChannels = getStringSet(PrefKeys.FOLLOWING_CHANNELS, null) ?: setOf() @@ -247,7 +251,7 @@ object LocalPreferences { val hideDeleteRequestDialog = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false) val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false) - return Account( + val a = Account( Persona(privKey = privKey?.toByteArray(), pubKey = pubKey.toByteArray()), followingChannels, hiddenUsers, @@ -261,23 +265,25 @@ object LocalPreferences { hideBlockAlertDialog, latestContactList ) + + return a } } fun saveLastRead(route: String, timestampInSecs: Long) { - encryptedPreferences(currentAccount).edit().apply { + encryptedPreferences(currentAccount()).edit().apply { putLong(PrefKeys.LAST_READ(route), timestampInSecs) }.apply() } fun loadLastRead(route: String): Long { - encryptedPreferences(currentAccount).run { + encryptedPreferences(currentAccount()).run { return getLong(PrefKeys.LAST_READ(route), 0) } } fun migrateSingleUserPrefs() { - if (currentAccount != null) return + if (currentAccount() != null) return val pubkey = encryptedPreferences().getString(PrefKeys.NOSTR_PUBKEY, null) ?: return val npub = Hex.decode(pubkey).toNpub() @@ -314,6 +320,6 @@ object LocalPreferences { encryptedPreferences().edit().clear().apply() addAccount(npub) - currentAccount = npub + updateCurrentAccount(npub) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt index ceb6631922..d50ce32bcf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape @@ -24,10 +23,8 @@ import androidx.compose.material.TextButton import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.icons.filled.Key import androidx.compose.material.icons.filled.Logout import androidx.compose.material.icons.filled.RadioButtonChecked -import androidx.compose.material.icons.filled.Visibility import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState @@ -45,14 +42,15 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.decodePublicKey +import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy import com.vitorpamplona.amethyst.ui.note.toShortenHex import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginPage -import nostr.postr.bechToBytes -import nostr.postr.toHex @Composable fun AccountSwitchBottomSheet( @@ -82,90 +80,101 @@ fun AccountSwitchBottomSheet( accounts.forEach { acc -> val current = accountUser.pubkeyNpub() == acc.npub - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { + val baseUser = try { + LocalCache.getOrCreateUser(decodePublicKey(acc.npub).toHexKey()) + } catch (e: Exception) { + null + } + + if (baseUser != null) { + val userState by baseUser.live().metadata.observeAsState() + val user = userState?.user ?: return + Row( - modifier = Modifier - .weight(1f) - .clickable { - accountStateViewModel.switchUser(acc.npub) - }, + modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { Row( modifier = Modifier - .padding(16.dp, 16.dp) - .weight(1f), + .weight(1f) + .clickable { + accountStateViewModel.switchUser(acc.npub) + }, verticalAlignment = Alignment.CenterVertically ) { - Box( + Row( modifier = Modifier - .width(55.dp) - .padding(0.dp) + .padding(16.dp, 16.dp) + .weight(1f), + verticalAlignment = Alignment.CenterVertically ) { - RobohashAsyncImageProxy( - robot = acc.npub.bechToBytes("npub").toHex(), - model = ResizeImage(acc.profilePicture, 55.dp), - contentDescription = stringResource(R.string.profile_image), - modifier = Modifier - .width(55.dp) - .height(55.dp) - .clip(shape = CircleShape) - ) Box( modifier = Modifier - .size(20.dp) - .align(Alignment.TopEnd) + .width(55.dp) + .padding(0.dp) ) { - if (acc.hasPrivKey) { + RobohashAsyncImageProxy( + robot = user.pubkeyHex, + model = ResizeImage(user.profilePicture(), 55.dp), + contentDescription = stringResource(R.string.profile_image), + modifier = Modifier + .width(55.dp) + .height(55.dp) + .clip(shape = CircleShape) + )/* + Box( + modifier = Modifier + .size(20.dp) + .align(Alignment.TopEnd) + ) { + if (acc.hasPrivKey) { + Icon( + imageVector = Icons.Default.Key, + contentDescription = stringResource(R.string.account_switch_has_private_key), + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colors.primary + ) + } else { + Icon( + imageVector = Icons.Default.Visibility, + contentDescription = stringResource(R.string.account_switch_pubkey_only), + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colors.primary + ) + } + }*/ + } + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + val npubShortHex = acc.npub.toShortenHex() + + user.bestDisplayName()?.let { + Text(it) + } + + Text(npubShortHex) + } + Column(modifier = Modifier.width(32.dp)) { + if (current) { Icon( - imageVector = Icons.Default.Key, - contentDescription = stringResource(R.string.account_switch_has_private_key), - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colors.primary - ) - } else { - Icon( - imageVector = Icons.Default.Visibility, - contentDescription = stringResource(R.string.account_switch_pubkey_only), - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colors.primary + imageVector = Icons.Default.RadioButtonChecked, + contentDescription = stringResource(R.string.account_switch_active_account), + tint = MaterialTheme.colors.secondary ) } } } - Spacer(modifier = Modifier.width(16.dp)) - Column(modifier = Modifier.weight(1f)) { - val npubShortHex = acc.npub.toShortenHex() - - if (acc.displayName != null && acc.displayName != npubShortHex) { - Text(acc.displayName) - } - - Text(npubShortHex) - } - Column(modifier = Modifier.width(32.dp)) { - if (current) { - Icon( - imageVector = Icons.Default.RadioButtonChecked, - contentDescription = stringResource(R.string.account_switch_active_account), - tint = MaterialTheme.colors.secondary - ) - } - } } - } - IconButton( - onClick = { accountStateViewModel.logOff(acc.npub) } - ) { - Icon( - imageVector = Icons.Default.Logout, - contentDescription = stringResource(R.string.log_out), - tint = MaterialTheme.colors.onSurface - ) + IconButton( + onClick = { accountStateViewModel.logOff(acc.npub) } + ) { + Icon( + imageVector = Icons.Default.Logout, + contentDescription = stringResource(R.string.log_out), + tint = MaterialTheme.colors.onSurface + ) + } } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index 58c9140e97..0118af4591 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -35,11 +35,11 @@ class AccountStateViewModel() : ViewModel() { private fun tryLoginExistingAccount() { LocalPreferences.loadFromEncryptedStorage()?.let { - login(it) + startUI(it) } } - fun login(key: String) { + fun startUI(key: String) { val pattern = Pattern.compile(".+@.+\\.[a-z]+") val parsed = Nip19.uriToRoute(key) val pubKeyParsed = parsed?.hex?.toByteArray() @@ -56,7 +56,8 @@ class AccountStateViewModel() : ViewModel() { Account(Persona(Hex.decode(key))) } - login(account) + LocalPreferences.updatePrefsForLogin(account) + startUI(account) } fun switchUser(npub: String) { @@ -67,24 +68,22 @@ class AccountStateViewModel() : ViewModel() { fun newKey() { val account = Account(Persona()) - login(account) + // saves to local preferences + LocalPreferences.updatePrefsForLogin(account) + startUI(account) } @OptIn(DelicateCoroutinesApi::class) - fun login(account: Account) { - LocalPreferences.updatePrefsForLogin(account) - + fun startUI(account: Account) { if (account.loggedIn.privKey != null) { _accountContent.update { AccountState.LoggedIn(account) } } else { _accountContent.update { AccountState.LoggedInViewOnly(account) } } - val scope = CoroutineScope(Job() + Dispatchers.IO) scope.launch { ServiceManager.start(account) } - GlobalScope.launch(Dispatchers.Main) { account.saveable.observeForever(saveListener) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt index 844df8e4d2..ec45c3aff3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt @@ -158,7 +158,7 @@ fun LoginPage( keyboardActions = KeyboardActions( onGo = { try { - accountViewModel.login(key.value.text) + accountViewModel.startUI(key.value.text) } catch (e: Exception) { errorMessage = context.getString(R.string.invalid_key) } @@ -237,7 +237,7 @@ fun LoginPage( if (acceptedTerms.value && key.value.text.isNotBlank()) { try { - accountViewModel.login(key.value.text) + accountViewModel.startUI(key.value.text) } catch (e: Exception) { errorMessage = context.getString(R.string.invalid_key) } From 292ce779a1bf0cf48465832cf00b60966a8da3fd Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 5 Apr 2023 08:45:33 -0400 Subject: [PATCH 068/145] Lazy initialization of the json mapper (300ms) --- .../java/com/vitorpamplona/amethyst/model/LocalCache.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 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 d13b3c7d61..bced78258b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -17,9 +17,11 @@ import java.time.format.DateTimeFormatter import java.util.concurrent.ConcurrentHashMap object LocalCache { - val metadataParser = jacksonObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .readerFor(UserMetadata::class.java) + val metadataParser by lazy { + jacksonObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .readerFor(UserMetadata::class.java) + } val antiSpam = AntiSpamFilter() From 7a957bd6ecd38ac4e4a7b5106dfefe2cba029177 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 5 Apr 2023 08:45:58 -0400 Subject: [PATCH 069/145] Initializing indexes with large sizes from the start. --- .../java/com/vitorpamplona/amethyst/model/LocalCache.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 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 bced78258b..4a5e93e2e9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -25,10 +25,10 @@ object LocalCache { val antiSpam = AntiSpamFilter() - val users = ConcurrentHashMap() - val notes = ConcurrentHashMap() + val users = ConcurrentHashMap(5000) + val notes = ConcurrentHashMap(5000) val channels = ConcurrentHashMap() - val addressables = ConcurrentHashMap() + val addressables = ConcurrentHashMap(100) fun checkGetOrCreateUser(key: String): User? { if (isValidHexNpub(key)) { @@ -602,8 +602,7 @@ object LocalCache { val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } + event.taggedAddresses().map { getOrCreateAddressableNote(it) } + ( - (zapRequest?.event as? LnZapRequestEvent)?.taggedAddresses() - ?.map { getOrCreateAddressableNote(it) } ?: emptySet() + (zapRequest?.event as? LnZapRequestEvent)?.taggedAddresses()?.map { getOrCreateAddressableNote(it) } ?: emptySet() ) note.loadEvent(event, author, repliesTo) From 65910295db6ac7a98c5a89ecf47cfe013db1a4cf Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 5 Apr 2023 08:46:25 -0400 Subject: [PATCH 070/145] Adds notification for Zaps that don't have a Note attached to them. --- .../amethyst/ui/note/ZapUserSetCompose.kt | 109 ++++++++++++++++++ .../amethyst/ui/screen/CardFeedState.kt | 9 ++ .../amethyst/ui/screen/CardFeedView.kt | 8 ++ .../amethyst/ui/screen/CardFeedViewModel.kt | 26 ++++- 4 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt new file mode 100644 index 0000000000..f9512cde8a --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt @@ -0,0 +1,109 @@ +package com.vitorpamplona.amethyst.ui.note + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Bolt +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.compositeOver +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import com.google.accompanist.flowlayout.FlowRow +import com.vitorpamplona.amethyst.NotificationCache +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.screen.ZapUserSetCard +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun ZapUserSetCompose(zapSetCard: ZapUserSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, navController: NavController) { + val accountState by accountViewModel.accountLiveData.observeAsState() + val account = accountState?.account ?: return + + var isNew by remember { mutableStateOf(false) } + + LaunchedEffect(key1 = zapSetCard) { + withContext(Dispatchers.IO) { + isNew = zapSetCard.createdAt > NotificationCache.load(routeForLastRead) + + NotificationCache.markAsRead(routeForLastRead, zapSetCard.createdAt) + } + } + + var backgroundColor = if (isNew) { + MaterialTheme.colors.primary.copy(0.12f).compositeOver(MaterialTheme.colors.background) + } else { + MaterialTheme.colors.background + } + + Column( + modifier = Modifier + .background(backgroundColor) + .clickable { + navController.navigate("User/${zapSetCard.user.pubkeyHex}") + } + ) { + Row( + modifier = Modifier + .padding( + start = if (!isInnerNote) 12.dp else 0.dp, + end = if (!isInnerNote) 12.dp else 0.dp, + top = 10.dp + ) + ) { + // Draws the like picture outside the boosted card. + if (!isInnerNote) { + Box( + modifier = Modifier + .width(55.dp) + .padding(0.dp) + ) { + Icon( + imageVector = Icons.Default.Bolt, + contentDescription = stringResource(id = R.string.zaps), + tint = BitcoinOrange, + modifier = Modifier + .size(25.dp) + .align(Alignment.TopEnd) + ) + } + } + + Column(modifier = Modifier.padding(start = if (!isInnerNote) 10.dp else 0.dp)) { + FlowRow() { + zapSetCard.zapEvents.forEach { + NoteAuthorPicture( + note = it.key, + navController = navController, + userAccount = account.userProfile(), + size = 35.dp + ) + } + } + + UserCompose(baseUser = zapSetCard.user, accountViewModel = accountViewModel, navController = navController) + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedState.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedState.kt index bdacb26395..3f374730c2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedState.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedState.kt @@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.screen import androidx.compose.runtime.MutableState import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User abstract class Card() { abstract fun createdAt(): Long @@ -40,6 +41,14 @@ class ZapSetCard(val note: Note, val zapEvents: Map) : Card() { override fun id() = note.idHex + "Z" + createdAt } +class ZapUserSetCard(val user: User, val zapEvents: Map) : Card() { + val createdAt = zapEvents.maxOf { it.value.createdAt() ?: 0 } + override fun createdAt(): Long { + return createdAt + } + override fun id() = user.pubkeyHex + "U" + createdAt +} + class MultiSetCard(val note: Note, val boostEvents: List, val likeEvents: List, val zapEvents: Map) : Card() { val createdAt = maxOf( zapEvents.maxOfOrNull { it.value.createdAt() ?: 0 } ?: 0, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt index ed5e46c629..fb37977595 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt @@ -30,6 +30,7 @@ import com.vitorpamplona.amethyst.ui.note.MessageSetCompose import com.vitorpamplona.amethyst.ui.note.MultiSetCompose import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.ZapSetCompose +import com.vitorpamplona.amethyst.ui.note.ZapUserSetCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @OptIn(ExperimentalMaterialApi::class) @@ -128,6 +129,13 @@ private fun FeedLoaded( navController = navController, routeForLastRead = routeForLastRead ) + is ZapUserSetCard -> ZapUserSetCompose( + item, + isInnerNote = false, + accountViewModel = accountViewModel, + navController = navController, + routeForLastRead = routeForLastRead + ) is LikeSetCard -> LikeSetCompose( item, isInnerNote = false, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt index 4e547522d3..3c83d09452 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt @@ -7,6 +7,7 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCacheState 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.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent @@ -78,7 +79,7 @@ open class CardFeedViewModel(val dataSource: FeedFilter) : ViewModel() { } // val reactionCards = reactionsPerEvent.map { LikeSetCard(it.key, it.value) } - + val zapsPerUser = mutableMapOf>() val zapsPerEvent = mutableMapOf>() notes .filter { it.event is LnZapEvent } @@ -89,6 +90,20 @@ open class CardFeedViewModel(val dataSource: FeedFilter) : ViewModel() { if (zapRequest != null) { zapsPerEvent.getOrPut(zappedPost, { mutableMapOf() }).put(zapRequest, zapEvent) } + } else { + val event = (zapEvent.event as LnZapEvent) + val author = event.zappedAuthor().mapNotNull { + LocalCache.checkGetOrCreateUser( + it + ) + }.firstOrNull() + if (author != null) { + val zapRequest = author.zaps.filter { it.value == zapEvent }.keys.firstOrNull() + if (zapRequest != null) { + zapsPerUser.getOrPut(author, { mutableMapOf() }) + .put(zapRequest, zapEvent) + } + } } } @@ -121,6 +136,13 @@ open class CardFeedViewModel(val dataSource: FeedFilter) : ViewModel() { } }.flatten() + val userZaps = zapsPerUser.map { + ZapUserSetCard( + it.key, + it.value + ) + } + val textNoteCards = notes.filter { it.event !is ReactionEvent && it.event !is RepostEvent && it.event !is LnZapEvent }.map { if (it.event is PrivateDmEvent) { MessageSetCard(it) @@ -131,7 +153,7 @@ open class CardFeedViewModel(val dataSource: FeedFilter) : ViewModel() { } } - return (multiCards + textNoteCards).sortedBy { it.createdAt() }.reversed() + return (multiCards + textNoteCards + userZaps).sortedBy { it.createdAt() }.reversed() } private fun updateFeed(notes: List) { From b0953310c2d2eb82131e204d35f6d81b7832c0c3 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 5 Apr 2023 11:10:14 -0400 Subject: [PATCH 071/145] Making the poll click animation wider. --- .../java/com/vitorpamplona/amethyst/ui/note/PollNote.kt | 6 ++---- .../java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt | 6 ++---- 2 files changed, 4 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 b39b1aea27..d4206c4822 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 @@ -2,7 +2,6 @@ package com.vitorpamplona.amethyst.ui.note import android.widget.Toast 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 @@ -10,7 +9,6 @@ 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.runtime.saveable.rememberSaveable @@ -188,8 +186,8 @@ fun ZapVote( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.combinedClickable( role = Role.Button, - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false, radius = 24.dp), + // interactionSource = remember { MutableInteractionSource() }, + // indication = rememberRipple(bounded = false, radius = 24.dp), onClick = { if (!accountViewModel.isWriteable()) { scope.launch { 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 74748ab494..3192a19344 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 @@ -64,7 +64,6 @@ import kotlinx.coroutines.withContext import java.math.BigDecimal import java.math.RoundingMode -@OptIn(ExperimentalFoundationApi::class) @Composable fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel) { val accountState by accountViewModel.accountLiveData.observeAsState() @@ -125,7 +124,7 @@ fun ReplyReaction( val scope = rememberCoroutineScope() IconButton( - modifier = Modifier.then(Modifier.size(20.dp)), + modifier = Modifier.size(20.dp), onClick = { if (accountViewModel.isWriteable()) { onPress() @@ -460,7 +459,7 @@ private fun ViewCountReaction(baseNote: Note, textModifier: Modifier = Modifier) val grayTint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) IconButton( - modifier = Modifier.then(Modifier.size(20.dp)), + modifier = Modifier.size(20.dp), onClick = { uri.openUri("https://counter.amethyst.social/${baseNote.idHex}/") } ) { Icon( @@ -475,7 +474,6 @@ private fun ViewCountReaction(baseNote: Note, textModifier: Modifier = Modifier) AsyncImage( model = ImageRequest.Builder(LocalContext.current) .data("https://counter.amethyst.social/${baseNote.idHex}.svg?label=+&color=00000000") - .crossfade(true) .diskCachePolicy(CachePolicy.DISABLED) .memoryCachePolicy(CachePolicy.ENABLED) .build(), From 2bda50ad4acb28e74a437692ad398ea58fea12dc Mon Sep 17 00:00:00 2001 From: vincivermeer <130012497+vincivermeer@users.noreply.github.com> Date: Thu, 6 Apr 2023 00:11:49 -0300 Subject: [PATCH 072/145] add contributors in README.md Hello everyone! I added the contributors in the readme.md file in the repository: vitorpamplona/amethyst. I would like to contribute to this pull-request and I hope you all accept it. what do you all think of this idea? --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 795acbae60..27b39d5a28 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,12 @@ Information shared on nostr should be assumed permanent for privacy purposes. Th [Issues](https://github.com/vitorpamplona/amethyst/issues) and [pull requests](https://github.com/vitorpamplona/amethyst/pulls) are very welcome. +## Contributors + + + + + # MIT License Copyright (c) 2023 Vitor Pamplona From 8344274011434bc1dd4451ca5064a9b160591a17 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 6 Apr 2023 11:54:28 -0400 Subject: [PATCH 073/145] Adds Invoice Creation to New Posts. --- .../com/vitorpamplona/amethyst/model/User.kt | 4 + .../amethyst/ui/actions/NewMessageTagger.kt | 80 +++++++++++++++++++ .../amethyst/ui/actions/NewPostView.kt | 60 ++++++++++++++ .../amethyst/ui/actions/NewPostViewModel.kt | 12 ++- .../amethyst/ui/components/InvoiceRequest.kt | 25 +++--- .../ui/screen/loggedIn/ProfileScreen.kt | 24 +++++- app/src/main/res/values/strings.xml | 2 + 7 files changed, 190 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt index 909a4d7050..476f4ef7d0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -387,6 +387,10 @@ class UserMetadata { return listOfNotNull(name, username, display_name, displayName, nip05, lud06, lud16) .any { it.startsWith(prefix, true) } } + + fun lnAddress(): String? { + return (lud16?.trim() ?: lud06?.trim())?.ifBlank { null } + } } class UserLiveData(val user: User) : LiveData(UserState(user)) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt new file mode 100644 index 0000000000..dc39228e0b --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt @@ -0,0 +1,80 @@ +package com.vitorpamplona.amethyst.ui.actions + +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.parseDirtyWordForKey +import com.vitorpamplona.amethyst.service.nip19.Nip19 + +class NewMessageProcessor(var originalNote: Note?, var mentions: List?, var replyTos: List?, var message: String) { + + open fun addUserToMentions(user: User) { + mentions = if (mentions?.contains(user) == true) mentions else mentions?.plus(user) ?: listOf(user) + } + + open fun addNoteToReplyTos(note: Note) { + note.author?.let { addUserToMentions(it) } + replyTos = if (replyTos?.contains(note) == true) replyTos else replyTos?.plus(note) ?: listOf(note) + } + + 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) + } + + 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 run() { + // adds all references to mentions and reply tos + message.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.EVENT) { + 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. + message = message.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.EVENT) { + 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") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index fba0b3fa13..8549dd0ec6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -12,6 +12,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MonetizationOn import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember @@ -29,6 +31,7 @@ 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.input.TextFieldValue import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog @@ -182,6 +185,28 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n } } + val user = postViewModel.account?.userProfile() + val lud16 = user?.info?.lnAddress() + + if (lud16 != null && user != null && postViewModel.wantsInvoice) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 5.dp)) { + InvoiceRequest( + lud16, + user.pubkeyHex, + account, + stringResource(id = R.string.lightning_invoice), + stringResource(id = R.string.lightning_create_and_add_invoice), + onSuccess = { + postViewModel.message = TextFieldValue(postViewModel.message.text + "\n\n" + it) + postViewModel.wantsInvoice = false + }, + onClose = { + postViewModel.wantsInvoice = false + } + ) + } + } + val myUrlPreview = postViewModel.urlPreview if (myUrlPreview != null) { Row(modifier = Modifier.padding(top = 5.dp)) { @@ -249,6 +274,12 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n postViewModel.wantsPoll = !postViewModel.wantsPoll } } + + if (postViewModel.canAddLnInvoice()) { + AddLnInvoiceButton(postViewModel.wantsInvoice) { + postViewModel.wantsInvoice = !postViewModel.wantsInvoice + } + } } } } @@ -284,6 +315,35 @@ private fun AddPollButton( } } + +@Composable +private fun AddLnInvoiceButton( + isLnInvoiceActive: Boolean, + onClick: () -> Unit +) { + IconButton( + onClick = { + onClick() + } + ) { + if (!isLnInvoiceActive) { + Icon( + imageVector = Icons.Default.MonetizationOn, + null, + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colors.onBackground + ) + } else { + Icon( + imageVector = Icons.Default.MonetizationOn, + null, + modifier = Modifier.size(20.dp), + tint = Color.Green.copy(alpha = 0.52f) + ) + } + } +} + @Composable fun CloseButton(onCancel: () -> Unit) { Button( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index 21f4799316..fbb964c5b1 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 @@ -53,6 +53,10 @@ open class NewPostViewModel : ViewModel() { var isValidConsensusThreshold = mutableStateOf(true) var isValidClosedAt = mutableStateOf(true) + // Invoices + var wantsInvoice by mutableStateOf(false) + + open fun load(account: Account, replyingTo: Note?, quote: Note?) { originalNote = replyingTo replyingTo?.let { replyNote -> @@ -199,6 +203,8 @@ open class NewPostViewModel : ViewModel() { valueMinimum = null consensusThreshold = null closedAt = null + + wantsInvoice = false } open fun findUrlInMessage(): String? { @@ -248,11 +254,15 @@ open class NewPostViewModel : ViewModel() { } fun canPost(): Boolean { - return message.text.isNotBlank() && !isUploadingImage && + return message.text.isNotBlank() && !isUploadingImage && !wantsInvoice && (!wantsPoll || pollOptions.values.all { it.isNotEmpty() }) } fun canUsePoll(): Boolean { return originalNote?.event !is PrivateDmEvent && originalNote?.channel() == null } + + fun canAddLnInvoice(): Boolean { + return account?.userProfile()?.info?.lnAddress() != null + } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt index 2fff33f834..bca274b3c6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt @@ -1,7 +1,5 @@ package com.vitorpamplona.amethyst.ui.components -import android.content.Intent -import android.net.Uri import android.widget.Toast import androidx.compose.foundation.border import androidx.compose.foundation.layout.Column @@ -36,14 +34,21 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.core.content.ContextCompat.startActivity import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver import kotlinx.coroutines.launch @Composable -fun InvoiceRequest(lud16: String, toUserPubKeyHex: String, account: Account, onClose: () -> Unit) { +fun InvoiceRequest( + lud16: String, + toUserPubKeyHex: String, + account: Account, + titleText: String? = null, + buttonText: String? = null, + onSuccess: (String) -> Unit, + onClose: () -> Unit +) { val context = LocalContext.current val scope = rememberCoroutineScope() @@ -73,7 +78,7 @@ fun InvoiceRequest(lud16: String, toUserPubKeyHex: String, account: Account, onC ) Text( - text = stringResource(R.string.lightning_tips), + text = titleText ?: stringResource(R.string.lightning_tips), fontSize = 20.sp, fontWeight = FontWeight.W500, modifier = Modifier.padding(start = 10.dp) @@ -137,13 +142,7 @@ fun InvoiceRequest(lud16: String, toUserPubKeyHex: String, account: Account, onC amount * 1000, message, zapRequest?.toJson(), - onSuccess = { - runCatching { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$it")) - startActivity(context, intent, null) - } - onClose() - }, + onSuccess = onSuccess, onError = { scope.launch { Toast.makeText(context, it, Toast.LENGTH_SHORT).show() @@ -159,7 +158,7 @@ fun InvoiceRequest(lud16: String, toUserPubKeyHex: String, account: Account, onC backgroundColor = MaterialTheme.colors.primary ) ) { - Text(text = stringResource(R.string.send_sats), color = Color.White, fontSize = 20.sp) + Text(text = buttonText ?: stringResource(R.string.send_sats), color = Color.White, fontSize = 20.sp) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index 8f262e310e..3f60fbb908 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -1,5 +1,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn +import android.content.Intent +import android.net.Uri import androidx.compose.foundation.* import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.layout.* @@ -27,6 +29,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalUriHandler @@ -39,6 +42,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.core.content.ContextCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.viewmodel.compose.viewModel @@ -421,6 +425,7 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account, accountViewMode val uri = LocalUriHandler.current val clipboardManager = LocalClipboardManager.current + val context = LocalContext.current Row(verticalAlignment = Alignment.Bottom) { user.bestDisplayName()?.let { @@ -555,9 +560,22 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account, accountViewMode if (zapExpanded) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 5.dp)) { - InvoiceRequest(lud16, baseUser.pubkeyHex, account) { - zapExpanded = false - } + InvoiceRequest(lud16, baseUser.pubkeyHex, account, + onSuccess = { + // pay directly + if (account.hasWalletConnectSetup()) { + account.sendZapPaymentRequestFor(it) + } else { + runCatching { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$it")) + ContextCompat.startActivity(context, intent, null) + } + } + }, + onClose = { + zapExpanded = false + } + ) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 40e656b03d..6e8947047c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -288,4 +288,6 @@ Add a public message Thank you for all your work! + + Create and Add From 7012d949c27172066cf27ab76e2e116db7c7a171 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 6 Apr 2023 11:56:23 -0400 Subject: [PATCH 074/145] Fixes formatting --- .../com/vitorpamplona/amethyst/ui/actions/NewPostView.kt | 1 - .../amethyst/ui/screen/loggedIn/ProfileScreen.kt | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index 8549dd0ec6..613af9d3d7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -315,7 +315,6 @@ private fun AddPollButton( } } - @Composable private fun AddLnInvoiceButton( isLnInvoiceActive: Boolean, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index 3f60fbb908..6c832ceac1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -560,7 +560,10 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account, accountViewMode if (zapExpanded) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 5.dp)) { - InvoiceRequest(lud16, baseUser.pubkeyHex, account, + InvoiceRequest( + lud16, + baseUser.pubkeyHex, + account, onSuccess = { // pay directly if (account.hasWalletConnectSetup()) { From 8676752f19a62da1298a5b1ba92253b0cb393c94 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 6 Apr 2023 11:57:28 -0400 Subject: [PATCH 075/145] Fixes Channel's reply to filtration (removes citations and the channel id from replies) --- .../main/java/com/vitorpamplona/amethyst/model/LocalCache.kt | 4 ++-- .../amethyst/service/model/BaseTextNoteEvent.kt | 2 +- .../amethyst/service/model/ChannelMessageEvent.kt | 5 ++--- 3 files changed, 5 insertions(+), 6 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 4a5e93e2e9..cef63d463f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -565,9 +565,9 @@ object LocalCache { return } - val replyTo = event.replyTos() + val replyTo = event.tagsWithoutCitations() + .filter { it != event.channel() } .mapNotNull { checkGetOrCreateNote(it) } - .filter { it.event !is ChannelCreateEvent } note.loadEvent(event, author, replyTo) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/BaseTextNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/BaseTextNoteEvent.kt index 4583e39d82..c401ff260e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/BaseTextNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/BaseTextNoteEvent.kt @@ -13,7 +13,7 @@ open class BaseTextNoteEvent( sig: HexKey ) : Event(id, pubKey, createdAt, kind, tags, content, sig) { fun mentions() = taggedUsers() - fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } + open fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } fun findCitations(): Set { var citations = mutableSetOf() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMessageEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMessageEvent.kt index 76a05c6cd5..e2db58fbdf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMessageEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMessageEvent.kt @@ -12,11 +12,10 @@ class ChannelMessageEvent( tags: List>, content: String, sig: HexKey -) : Event(id, pubKey, createdAt, kind, tags, content, sig) { +) : BaseTextNoteEvent(id, pubKey, createdAt, kind, tags, content, sig) { fun channel() = tags.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1) ?: tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1) - fun replyTos() = tags.filter { it.getOrNull(1) != channel() }.mapNotNull { it.getOrNull(1) } - fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } + override fun replyTos() = tags.filter { it.firstOrNull() == "e" && it.getOrNull(1) != channel() }.mapNotNull { it.getOrNull(1) } companion object { const val kind = 42 From 6cf1960df4354eec125f94d9a8f49c12f4904a15 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 6 Apr 2023 11:58:49 -0400 Subject: [PATCH 076/145] Adds a New Message Tagger to Chats (tags Notes and Users by default) and Private Message (doesn't tag Users and Notes) --- .../vitorpamplona/amethyst/model/Account.kt | 5 +- .../amethyst/ui/actions/NewMessageTagger.kt | 131 +++++++++--------- .../amethyst/ui/actions/NewPostViewModel.kt | 79 +---------- .../ui/note/ChatroomMessageCompose.kt | 20 ++- .../ui/screen/loggedIn/ChannelScreen.kt | 5 +- 5 files changed, 88 insertions(+), 152 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 442688933b..5239087e4f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -402,10 +402,11 @@ class Account( LocalCache.consume(signedEvent) } - fun sendChannelMessage(message: String, toChannel: String, replyingTo: Note? = null, mentions: List?) { + fun sendChannelMessage(message: String, toChannel: String, replyTo: List?, mentions: List?) { if (!isWriteable()) return - val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null } + // val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null } + val repliesToHex = replyTo?.map { it.idHex } val mentionsHex = mentions?.map { it.pubkeyHex } val signedEvent = ChannelMessageEvent.create( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt index dc39228e0b..516ac6e52e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt @@ -1,80 +1,81 @@ package com.vitorpamplona.amethyst.ui.actions +import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.parseDirtyWordForKey import com.vitorpamplona.amethyst.service.nip19.Nip19 -class NewMessageProcessor(var originalNote: Note?, var mentions: List?, var replyTos: List?, var message: String) { +class NewMessageTagger(var channel: Channel?, var mentions: List?, var replyTos: List?, var message: String) { - open fun addUserToMentions(user: User) { - mentions = if (mentions?.contains(user) == true) mentions else mentions?.plus(user) ?: listOf(user) - } - - open fun addNoteToReplyTos(note: Note) { - note.author?.let { addUserToMentions(it) } - replyTos = if (replyTos?.contains(note) == true) replyTos else replyTos?.plus(note) ?: listOf(note) - } - - 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) - } - - 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 run() { - // adds all references to mentions and reply tos - message.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.EVENT) { - 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) - } - } - } + open fun addUserToMentions(user: User) { + mentions = if (mentions?.contains(user) == true) mentions else mentions?.plus(user) ?: listOf(user) } - // Tags the text in the correct order. - message = message.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) + open fun addNoteToReplyTos(note: Note) { + note.author?.let { addUserToMentions(it) } + replyTos = if (replyTos?.contains(note) == true) replyTos else replyTos?.plus(note) ?: listOf(note) + } - "#[${tagIndex(user)}]${results.restOfWord}" - } else if (results?.key?.type == Nip19.Type.NOTE) { - val note = LocalCache.getOrCreateNote(results.key.hex) + open fun tagIndex(user: User): Int { + // Postr Events assembles replies before mentions in the tag order + return (if (channel != null) 1 else 0) + (replyTos?.size ?: 0) + (mentions?.indexOf(user) ?: 0) + } - "#[${tagIndex(note)}]${results.restOfWord}" - } else if (results?.key?.type == Nip19.Type.EVENT) { - val note = LocalCache.getOrCreateNote(results.key.hex) + open fun tagIndex(note: Note): Int { + // Postr Events assembles replies before mentions in the tag order + return (if (channel != null) 1 else 0) + (replyTos?.indexOf(note) ?: 0) + } - "#[${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 + fun run() { + // adds all references to mentions and reply tos + message.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.EVENT) { + 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) + } + } + } } - }.joinToString(" ") - }.joinToString("\n") - } -} \ No newline at end of file + + // Tags the text in the correct order. + message = message.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.EVENT) { + 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") + } +} 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 fbb964c5b1..5c0a13b167 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 @@ -15,7 +15,6 @@ import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.* import com.vitorpamplona.amethyst.service.model.PrivateDmEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent -import com.vitorpamplona.amethyst.service.nip19.Nip19 import com.vitorpamplona.amethyst.ui.components.isValidURL import com.vitorpamplona.amethyst.ui.components.noProtocolUrlValidator import kotlinx.coroutines.Dispatchers @@ -56,7 +55,6 @@ open class NewPostViewModel : ViewModel() { // Invoices var wantsInvoice by mutableStateOf(false) - open fun load(account: Account, replyingTo: Note?, quote: Note?) { originalNote = replyingTo replyingTo?.let { replyNote -> @@ -84,83 +82,18 @@ open class NewPostViewModel : ViewModel() { this.account = account } - open fun addUserToMentions(user: User) { - mentions = if (mentions?.contains(user) == true) mentions else mentions?.plus(user) ?: listOf(user) - } - - open fun addNoteToReplyTos(note: Note) { - note.author?.let { addUserToMentions(it) } - replyTos = if (replyTos?.contains(note) == true) replyTos else replyTos?.plus(note) ?: listOf(note) - } - - 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) - } - - 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() { - // 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.EVENT) { - 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.EVENT) { - 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") + val tagger = NewMessageTagger(originalNote?.channel(), mentions, replyTos, message.text) + tagger.run() if (wantsPoll) { - account?.sendPoll(newMessage, replyTos, mentions, pollOptions, valueMaximum, valueMinimum, consensusThreshold, closedAt) + account?.sendPoll(tagger.message, tagger.replyTos, tagger.mentions, pollOptions, valueMaximum, valueMinimum, consensusThreshold, closedAt) } else if (originalNote?.channel() != null) { - account?.sendChannelMessage(newMessage, originalNote!!.channel()!!.idHex, originalNote!!, mentions) + account?.sendChannelMessage(tagger.message, tagger.channel!!.idHex, tagger.replyTos, tagger.mentions) } else if (originalNote?.event is PrivateDmEvent) { - account?.sendPrivateMessage(newMessage, originalNote!!.author!!.pubkeyHex, originalNote!!, mentions) + account?.sendPrivateMessage(tagger.message, originalNote!!.author!!.pubkeyHex, originalNote!!, tagger.mentions) } else { - account?.sendPost(newMessage, replyTos, mentions) + account?.sendPost(tagger.message, tagger.replyTos, tagger.mentions) } cancel() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index 6c5f6a6338..fee45ca0e9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -230,17 +230,15 @@ fun ChatroomMessageCompose( if (!innerQuote && !replyTo.isNullOrEmpty()) { Row(verticalAlignment = Alignment.CenterVertically) { replyTo.toSet().mapIndexed { _, note -> - if (note.event != null) { - ChatroomMessageCompose( - note, - null, - innerQuote = true, - parentBackgroundColor = backgroundBubbleColor, - accountViewModel = accountViewModel, - navController = navController, - onWantsToReply = onWantsToReply - ) - } + ChatroomMessageCompose( + note, + null, + innerQuote = true, + parentBackgroundColor = backgroundBubbleColor, + accountViewModel = accountViewModel, + navController = navController, + onWantsToReply = onWantsToReply + ) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index 547b3c447d..32c382803e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -66,6 +66,7 @@ import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.NostrChannelDataSource import com.vitorpamplona.amethyst.ui.actions.NewChannelView +import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel import com.vitorpamplona.amethyst.ui.actions.PostButton import com.vitorpamplona.amethyst.ui.actions.UploadFromGallery @@ -213,7 +214,9 @@ fun ChannelScreen( trailingIcon = { PostButton( onPost = { - account.sendChannelMessage(channelScreenModel.message.text, channel.idHex, replyTo.value, null) + val tagger = NewMessageTagger(channel, listOfNotNull(replyTo.value?.author), listOfNotNull(replyTo.value), channelScreenModel.message.text) + tagger.run() + account.sendChannelMessage(tagger.message, channel.idHex, tagger.replyTos, tagger.mentions) channelScreenModel.message = TextFieldValue("") replyTo.value = null feedViewModel.invalidateData() // Don't wait a full second before updating From 403a1723deaea435456e24c1a53b5cb775cbe121 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 6 Apr 2023 11:59:21 -0400 Subject: [PATCH 077/145] Previews Notes that are not a citation using #[n] --- .../amethyst/ui/components/RichTextViewer.kt | 51 +++++++++++++++++-- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 1dc0ce1548..a77a187a8f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -188,7 +188,13 @@ fun RichTextViewer( } else if (word.length > 6 && Patterns.PHONE.matcher(word).matches()) { ClickablePhone(word) } else if (isBechLink(word)) { - BechLink(word, navController) + BechLink( + word, + canPreview, + backgroundColor, + accountViewModel, + navController + ) } else if (word.startsWith("#")) { if (tagIndex.matcher(word).matches() && tags != null) { TagLink( @@ -239,7 +245,13 @@ fun RichTextViewer( } else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) { ClickablePhone(word) } else if (isBechLink(word)) { - BechLink(word, navController) + BechLink( + word, + canPreview, + backgroundColor, + accountViewModel, + navController + ) } else if (word.startsWith("#")) { if (tagIndex.matcher(word).matches() && tags != null) { TagLink( @@ -285,19 +297,48 @@ private fun isArabic(text: String): Boolean { } fun isBechLink(word: String): Boolean { - val cleaned = word.removePrefix("@").removePrefix("nostr:").removePrefix("@").take(7).lowercase() + val cleaned = word.lowercase().removePrefix("@").removePrefix("nostr:").removePrefix("@").take(7) return listOf("npub1", "naddr1", "note1", "nprofile1", "nevent1").any { cleaned.startsWith(it) } } @Composable -fun BechLink(word: String, navController: NavController) { +fun BechLink(word: String, canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, navController: NavController) { val nip19Route = Nip19.uriToRoute(word) if (nip19Route == null) { Text(text = "$word ") } else { - ClickableRoute(nip19Route, navController) + if (nip19Route.type == Nip19.Type.NOTE || nip19Route.type == Nip19.Type.EVENT || nip19Route.type == Nip19.Type.ADDRESS) { + val note = LocalCache.checkGetOrCreateNote(nip19Route.hex) + if (note != null) { + if (canPreview) { + NoteCompose( + baseNote = note, + accountViewModel = accountViewModel, + modifier = Modifier + .padding(top = 2.dp, bottom = 0.dp, start = 0.dp, end = 0.dp) + .fillMaxWidth() + .clip(shape = RoundedCornerShape(15.dp)) + .border( + 1.dp, + MaterialTheme.colors.onSurface.copy(alpha = 0.12f), + RoundedCornerShape(15.dp) + ), + parentBackgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.05f) + .compositeOver(backgroundColor), + isQuotedNote = true, + navController = navController + ) + } else { + ClickableRoute(nip19Route, navController) + } + } else { + ClickableRoute(nip19Route, navController) + } + } else { + ClickableRoute(nip19Route, navController) + } } } From f224fe6b403956610caada5d1a49f85a82228748 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 6 Apr 2023 12:23:35 -0400 Subject: [PATCH 078/145] v0.32.2 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index cc536b0687..cf6a1ba851 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 33 - versionCode 116 - versionName "0.32.1" + versionCode 117 + versionName "0.32.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From ee94cc6bf66e290b93f4cdd1839786f044d71573 Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Fri, 7 Apr 2023 08:41:34 +0900 Subject: [PATCH 079/145] don't allow poll authors to vote in their own polls --- .../com/vitorpamplona/amethyst/ui/note/PollNote.kt | 12 +++++++++++- app/src/main/res/values/strings.xml | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) 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 d4206c4822..e46645a34e 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 @@ -71,7 +71,7 @@ fun PollNote( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 3.dp) ) { - if (zappedNote.author == account.userProfile() || zappedNote.isZappedBy(account.userProfile()) == true) { + if (zappedNote.author == account.userProfile() || zappedNote.isZappedBy(account.userProfile())) { ZapVote( baseNote, accountViewModel, @@ -209,6 +209,16 @@ fun ZapVote( ) .show() } + } else if (zappedNote?.author == account.userProfile()) { + scope.launch { + Toast + .makeText( + context, + context.getString(R.string.poll_author_no_vote), + Toast.LENGTH_SHORT + ) + .show() + } } else if (pollViewModel.isVoteAmountAtomic() && pollViewModel.isPollOptionZappedBy(pollOption, accountViewModel.userProfile())) { // only allow one vote per option when min==max, i.e. atomic vote amount specified scope.launch { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6e8947047c..60375a15a8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -290,4 +290,5 @@ Thank you for all your work! Create and Add + Poll authors can\'t vote in their own polls. From 0fc6757d4a048718ad1dd7da2e576c3c77030379 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 7 Apr 2023 14:22:16 -0400 Subject: [PATCH 080/145] Adds inHidden check for hex keys --- app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 5239087e4f..f287b3011f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -716,7 +716,8 @@ class Account( } } - fun isHidden(user: User) = user.pubkeyHex in hiddenUsers || user.pubkeyHex in transientHiddenUsers + fun isHidden(user: User) = isHidden(user.pubkeyHex) + fun isHidden(userHex: String) = userHex in hiddenUsers || userHex in transientHiddenUsers fun followingKeySet(): Set { return userProfile().cachedFollowingKeySet() ?: emptySet() From b8e4ccfc287630557980b4a8dbed4f590555b95f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 7 Apr 2023 14:22:48 -0400 Subject: [PATCH 081/145] Lint mentions cannot be null. --- .../main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5ea90982a8..7d299b8279 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 @@ -372,7 +372,7 @@ fun NoteComposeInner( ReplyInformation(note.replyTo, sortedMentions, account, navController) } Spacer(modifier = Modifier.height(5.dp)) - } else if (!makeItShort && noteEvent is ChannelMessageEvent && (note.replyTo != null || noteEvent.mentions() != null)) { + } else if (!makeItShort && noteEvent is ChannelMessageEvent && (note.replyTo != null || noteEvent.mentions().isNotEmpty())) { val sortedMentions = noteEvent.mentions() .mapNotNull { LocalCache.checkGetOrCreateUser(it) } .toSet() From cc44b4dd555b3891e805bd9405d7404df35e543f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 7 Apr 2023 14:23:19 -0400 Subject: [PATCH 082/145] Removes reposts from conversations --- .../amethyst/ui/dal/HomeConversationsFeedFilter.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 78df542bc5..4cbb5f5dc5 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 @@ -4,7 +4,6 @@ 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 object HomeConversationsFeedFilter : FeedFilter() { @@ -17,7 +16,7 @@ object HomeConversationsFeedFilter : FeedFilter() { return LocalCache.notes.values .filter { - (it.event is TextNoteEvent || it.event is PollNoteEvent || it.event is RepostEvent) && + (it.event is TextNoteEvent || it.event is PollNoteEvent) && (it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false)) && // && account.isAcceptable(it) // This filter follows only. No need to check if acceptable it.author?.let { !account.isHidden(it) } ?: true && From 9ee3da39108ac008767b46f65cb566544f7532f9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 7 Apr 2023 14:23:34 -0400 Subject: [PATCH 083/145] Uses the inHidden in Hex --- .../vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 f452a5c177..bff02cd2f1 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 @@ -21,7 +21,7 @@ object HomeNewThreadFeedFilter : FeedFilter() { (it.event is TextNoteEvent || it.event is RepostEvent || it.event is PollNoteEvent) && (it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false)) && // && account.isAcceptable(it) // This filter follows only. No need to check if acceptable - it.author?.let { !account.isHidden(it) } ?: true && + it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true && it.isNewThread() } @@ -30,7 +30,7 @@ object HomeNewThreadFeedFilter : FeedFilter() { (it.event is LongTextNoteEvent) && (it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false)) && // && account.isAcceptable(it) // This filter follows only. No need to check if acceptable - it.author?.let { !account.isHidden(it) } ?: true && + it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true && it.isNewThread() } From 58f15153870cd5dc389b66c29817e6eede27b444 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 7 Apr 2023 14:24:00 -0400 Subject: [PATCH 084/145] Displays feed performance measurement before clipping to 1000 objects --- .../main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt index e5f78f4e26..6547f00fc9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt @@ -8,11 +8,11 @@ abstract class FeedFilter() { @OptIn(ExperimentalTime::class) fun loadTop(): List { val (feed, elapsed) = measureTimedValue { - feed().take(1000) + feed() } Log.d("Time", "${this.javaClass.simpleName} Feed in $elapsed with ${feed.size} objects") - return feed + return feed.take(1000) } abstract fun feed(): List From 2424dc3961ce0657762a37ba3bbf19248fd8c755 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 7 Apr 2023 14:24:43 -0400 Subject: [PATCH 085/145] Brings the findCitedUsers function to BaseTextNote Event with cache. --- .../com/vitorpamplona/amethyst/model/Note.kt | 41 +------------------ .../service/model/BaseTextNoteEvent.kt | 24 ++++++++++- 2 files changed, 23 insertions(+), 42 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 d8f633116d..063a40e904 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -57,7 +57,7 @@ open class Note(val idHex: String) { val channelHex = (event as? ChannelMessageEvent)?.channel() ?: (event as? ChannelMetadataEvent)?.channel() - ?: (event as? ChannelCreateEvent)?.let { it.id } + ?: (event as? ChannelCreateEvent)?.id return channelHex?.let { LocalCache.checkGetOrCreateChannel(it) } } @@ -270,45 +270,6 @@ open class Note(val idHex: String) { ) } - fun directlyCiteUsersHex(): Set { - val matcher = tagSearch.matcher(event?.content() ?: "") - val returningList = mutableSetOf() - while (matcher.find()) { - try { - val tag = matcher.group(1)?.let { event?.tags()?.get(it.toInt()) } - if (tag != null && tag[0] == "p") { - returningList.add(tag[1]) - } - } catch (e: Exception) { - } - } - return returningList - } - - fun directlyCiteUsers(): Set { - val matcher = tagSearch.matcher(event?.content() ?: "") - val returningList = mutableSetOf() - while (matcher.find()) { - try { - val tag = matcher.group(1)?.let { event?.tags()?.get(it.toInt()) } - if (tag != null && tag[0] == "p") { - LocalCache.checkGetOrCreateUser(tag[1])?.let { - returningList.add(it) - } - } - } catch (e: Exception) { - } - } - return returningList - } - - fun directlyCites(userProfile: User): Boolean { - return author == userProfile || - (userProfile in directlyCiteUsers()) || - (event is ReactionEvent && replyTo?.lastOrNull()?.directlyCites(userProfile) == true) || - (event is RepostEvent && replyTo?.lastOrNull()?.directlyCites(userProfile) == true) - } - fun isNewThread(): Boolean { return event is RepostEvent || replyTo == null || replyTo?.size == 0 } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/BaseTextNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/BaseTextNoteEvent.kt index c401ff260e..031c2b1cf7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/BaseTextNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/BaseTextNoteEvent.kt @@ -15,6 +15,26 @@ open class BaseTextNoteEvent( fun mentions() = taggedUsers() open fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } + private var citedUsersCache: Set? = null + + fun citedUsers(): Set { + citedUsersCache?.let { return it } + + val matcher = tagSearch.matcher(content) + val returningList = mutableSetOf() + while (matcher.find()) { + try { + val tag = matcher.group(1)?.let { tags[it.toInt()] } + if (tag != null && tag.size > 1 && tag[0] == "p") { + returningList.add(tag[1]) + } + } catch (e: Exception) { + } + } + citedUsersCache = returningList + return returningList + } + fun findCitations(): Set { var citations = mutableSetOf() // Removes citations from replies: @@ -22,10 +42,10 @@ open class BaseTextNoteEvent( while (matcher.find()) { try { val tag = matcher.group(1)?.let { tags[it.toInt()] } - if (tag != null && tag[0] == "e") { + if (tag != null && tag.size > 1 && tag[0] == "e") { citations.add(tag[1]) } - if (tag != null && tag[0] == "a") { + if (tag != null && tag.size > 1 && tag[0] == "a") { citations.add(tag[1]) } } catch (e: Exception) { From a04fd4a9f23b5c0c80572942a987e104a53e6dc2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 7 Apr 2023 14:25:03 -0400 Subject: [PATCH 086/145] Matches parameter names to the Event class. --- .../vitorpamplona/amethyst/service/model/EventInterface.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventInterface.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventInterface.kt index 07194a0bf5..709a7dacd1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventInterface.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventInterface.kt @@ -24,11 +24,11 @@ interface EventInterface { fun hasValidSignature(): Boolean - fun isTaggedUser(loggedInUser: String): Boolean + fun isTaggedUser(idHex: String): Boolean fun isTaggedHash(hashtag: String): Boolean - fun isTaggedHashes(hashtag: Set): Boolean - fun firstIsTaggedHashes(hashtag: Set): String? + fun isTaggedHashes(hashtags: Set): Boolean + fun firstIsTaggedHashes(hashtags: Set): String? fun hashtags(): List fun getReward(): BigDecimal? From 28d501ed02ca67368b599b4cb55d9a4ae162bd81 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 7 Apr 2023 14:25:38 -0400 Subject: [PATCH 087/145] Uses more direct functions to get and compare tags as opposed to the fail safe options. --- .../amethyst/service/model/Event.kt | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 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 14fc3538f6..818a2d3b24 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 @@ -38,23 +38,23 @@ open class Event( override fun toJson(): String = gson.toJson(this) - fun taggedUsers() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } - fun taggedEvents() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } + fun taggedUsers() = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] } + fun taggedEvents() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] } - fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { - val aTagValue = it.getOrNull(1) + fun taggedAddresses() = tags.filter { it.size > 1 && it[0] == "a" }.mapNotNull { + val aTagValue = it[1] val relay = it.getOrNull(2) - if (aTagValue != null) ATag.parse(aTagValue, relay) else null + ATag.parse(aTagValue, relay) } - override fun hashtags() = tags.filter { it.firstOrNull() == "t" }.mapNotNull { it.getOrNull(1) } + override fun hashtags() = tags.filter { it.size > 1 && it[0] == "t" }.map { it[1] } - override fun isTaggedUser(idHex: String) = tags.any { it.getOrNull(0) == "p" && it.getOrNull(1) == idHex } + override fun isTaggedUser(idHex: String) = tags.any { it.size > 1 && it[0] == "p" && it[1] == idHex } - override fun isTaggedHash(hashtag: String) = tags.any { it.getOrNull(0) == "t" && it.getOrNull(1).equals(hashtag, true) } - override fun isTaggedHashes(hashtags: Set) = tags.any { it.getOrNull(0) == "t" && it.getOrNull(1)?.lowercase() in hashtags } - override fun firstIsTaggedHashes(hashtags: Set) = tags.firstOrNull { it.getOrNull(0) == "t" && it.getOrNull(1)?.lowercase() in hashtags }?.getOrNull(1) + override fun isTaggedHash(hashtag: String) = tags.any { it.size > 1 && it[0] == "t" && it[1].equals(hashtag, true) } + override fun isTaggedHashes(hashtags: Set) = tags.any { it.size > 1 && it[0] == "t" && it[1].lowercase() in hashtags } + override fun firstIsTaggedHashes(hashtags: Set) = tags.firstOrNull { it.size > 1 && it[0] == "t" && it[1].lowercase() in hashtags }?.getOrNull(1) override fun getPoWRank(): Int { var rank = 0 @@ -79,7 +79,7 @@ open class Event( override fun getReward(): BigDecimal? { return try { - tags.filter { it.firstOrNull() == "reward" }.mapNotNull { BigDecimal(it.getOrNull(1)) }.firstOrNull() + tags.filter { it.firstOrNull() == "reward" }.mapNotNull { it.getOrNull(1)?.let { BigDecimal(it) } }.firstOrNull() } catch (e: Exception) { null } From 3a082d131f972b1d137bc2c34658db72b8317b84 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 7 Apr 2023 14:26:07 -0400 Subject: [PATCH 088/145] Streamlines Notification filter for performance --- .../amethyst/ui/dal/NotificationFeedFilter.kt | 66 +++++++++---------- 1 file changed, 30 insertions(+), 36 deletions(-) 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 bbdc8b61ad..f1d53c0739 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 @@ -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.model.User import com.vitorpamplona.amethyst.service.model.* object NotificationFeedFilter : FeedFilter() { @@ -10,45 +11,38 @@ object NotificationFeedFilter : FeedFilter() { override fun feed(): List { val loggedInUser = account.userProfile() - return LocalCache.notes.values - .asSequence() - .filter { - it.event !is ChannelCreateEvent && - it.event !is ChannelMetadataEvent && - it.event !is LnZapRequestEvent && - it.event !is BadgeDefinitionEvent && - it.event !is BadgeProfilesEvent && - it.event?.isTaggedUser(loggedInUser.pubkeyHex) ?: false && - (it.author == null || (!account.isHidden(it.author!!) && it.author != loggedInUser)) - } - .filter { it -> - it.event !is TextNoteEvent || - (it.event as? TextNoteEvent)?.taggedEvents()?.any { - LocalCache.checkGetOrCreateNote(it)?.author == loggedInUser - } == true || - loggedInUser 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 == loggedInUser || - loggedInUser in it.directlyCiteUsers() - } - .filter { - it.event !is RepostEvent || - it.replyTo?.lastOrNull()?.author == loggedInUser || - loggedInUser in it.directlyCiteUsers() - } + val loggedInUserHex = loggedInUser.pubkeyHex + + return LocalCache.notes.values.filter { + it.event !is ChannelCreateEvent && + it.event !is ChannelMetadataEvent && + it.event !is LnZapRequestEvent && + it.event !is BadgeDefinitionEvent && + it.event !is BadgeProfilesEvent && + it.author !== loggedInUser && + it.event?.isTaggedUser(loggedInUserHex) ?: false && + (it.author == null || !account.isHidden(it.author!!.pubkeyHex)) && + tagsAnEventByUser(it, loggedInUser) + } .sortedBy { it.createdAt() } - .toList() .reversed() } - fun isDifferentAccount(account: Account): Boolean { - return this::account.isInitialized && this.account != account + fun tagsAnEventByUser(note: Note, author: User): Boolean { + val event = note.event + + if (event is BaseTextNoteEvent) { + return (event.citedUsers().contains(author.pubkeyHex) || note.replyTo?.any { it.author === author } == true) + } + + if (event is ReactionEvent) { + return note.replyTo?.lastOrNull()?.author === author + } + + if (event is RepostEvent) { + return note.replyTo?.lastOrNull()?.author === author + } + + return true } } From a5da4ea53d684db64babaefbcb98d199532d633d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 7 Apr 2023 14:26:23 -0400 Subject: [PATCH 089/145] Streamlines Global filter for performance --- .../amethyst/ui/dal/GlobalFeedFilter.kt | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) 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 f44b43bb45..e902c2f98c 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 @@ -3,10 +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.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.model.* object GlobalFeedFilter : FeedFilter() { lateinit var account: Account @@ -14,41 +11,43 @@ object GlobalFeedFilter : FeedFilter() { override fun feed(): List { val followChannels = account.followingChannels() val followUsers = account.followingKeySet() + val now = System.currentTimeMillis() / 1000 val notes = LocalCache.notes.values .asSequence() .filter { - (it.event is TextNoteEvent || it.event is LongTextNoteEvent || it.event is PollNoteEvent || it.event is ChannelMessageEvent) && - it.replyTo.isNullOrEmpty() + it.event is BaseTextNoteEvent && it.replyTo.isNullOrEmpty() } .filter { + val channel = it.channel() // does not show events already in the public chat list - (it.channel() == null || it.channel() !in followChannels) && + (channel == null || channel !in followChannels) && // does not show people the user already follows (it.author?.pubkeyHex !in followUsers) } .filter { account.isAcceptable(it) } .filter { // Do not show notes with the creation time exceeding the current time, as they will always stay at the top of the global feed, which is cheating. - it.createdAt()!! <= System.currentTimeMillis() / 1000 + it.createdAt()!! <= now } .toList() val longFormNotes = LocalCache.addressables.values .asSequence() .filter { - (it.event is LongTextNoteEvent) && it.replyTo.isNullOrEmpty() + it.event is LongTextNoteEvent && it.replyTo.isNullOrEmpty() } .filter { + val channel = it.channel() // does not show events already in the public chat list - (it.channel() == null || it.channel() !in followChannels) && + (channel == null || channel !in followChannels) && // does not show people the user already follows (it.author?.pubkeyHex !in followUsers) } .filter { account.isAcceptable(it) } .filter { // Do not show notes with the creation time exceeding the current time, as they will always stay at the top of the global feed, which is cheating. - it.createdAt()!! <= System.currentTimeMillis() / 1000 + it.createdAt()!! <= now } .toList() From 83f3af340ffd9af29618647dd650c6c7b48e7703 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 7 Apr 2023 16:55:15 -0400 Subject: [PATCH 090/145] Uses Kotlin's rounding function instead of java --- .../com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 3192a19344..e90641a386 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 @@ -63,6 +63,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.math.BigDecimal import java.math.RoundingMode +import kotlin.math.roundToInt @Composable fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel) { @@ -604,9 +605,9 @@ fun showCount(count: Int?): String { if (count == 0) return "" return when { - count >= 1000000000 -> "${Math.round(count / 1000000000f)}G" - count >= 1000000 -> "${Math.round(count / 1000000f)}M" - count >= 1000 -> "${Math.round(count / 1000f)}k" + count >= 1000000000 -> "${(count / 1000000000f).roundToInt()}G" + count >= 1000000 -> "${(count / 1000000f).roundToInt()}M" + count >= 1000 -> "${(count / 1000f).roundToInt()}k" else -> "$count" } } From d6b571590a0dd887c99684b198ecaf3798889f49 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 7 Apr 2023 16:55:46 -0400 Subject: [PATCH 091/145] Sends simpler object to help compose's stability --- .../java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8ac5fe9e3e..26d0448be4 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 @@ -365,7 +365,7 @@ fun NoteMaster( navController ) - DisplayUncitedHashtags(noteEvent, eventContent, navController) + DisplayUncitedHashtags(noteEvent.hashtags(), eventContent, navController) if (noteEvent is PollNoteEvent) { PollNote( From 88b80a0da1e7ef82a1eac2adfe8f6bfd3d5fe1cb Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 7 Apr 2023 16:56:08 -0400 Subject: [PATCH 092/145] Typo --- .../amethyst/ui/components/ClickableNoteTag.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableNoteTag.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableNoteTag.kt index bb3c99d0a3..8962fc219d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableNoteTag.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableNoteTag.kt @@ -11,12 +11,12 @@ import com.vitorpamplona.amethyst.ui.note.toShortenHex @Composable fun ClickableNoteTag( - baesNote: Note, + baseNote: Note, navController: NavController ) { ClickableText( - text = AnnotatedString("@${baesNote.idNote().toShortenHex()}"), - onClick = { navController.navigate("Note/${baesNote.idHex}") }, + text = AnnotatedString("@${baseNote.idNote().toShortenHex()}"), + onClick = { navController.navigate("Note/${baseNote.idHex}") }, style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary) ) } From e1ce638d7ffec81d5e14a9df7e49002f091922ad Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 7 Apr 2023 16:56:20 -0400 Subject: [PATCH 093/145] Compares by reference to simplify code --- app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 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 063a40e904..f12698a335 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -194,15 +194,15 @@ open class Note(val idHex: String) { fun isZappedBy(user: User): Boolean { // Zaps who the requester was the user - return zaps.any { it.key.author == user } + return zaps.any { it.key.author === user } } fun isReactedBy(user: User): Boolean { - return reactions.any { it.author == user } + return reactions.any { it.author === user } } fun isBoostedBy(user: User): Boolean { - return boosts.any { it.author == user } + return boosts.any { it.author === user } } fun reportsBy(user: User): Set { From dbf5267c5ce29f85ae069633dbee6f3f0de1fa11 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 7 Apr 2023 16:58:25 -0400 Subject: [PATCH 094/145] Moves LnInvoice, Tags, BechLinks and Previews to LaunchedEffect --- .../ui/components/ClickableWithdrawal.kt | 26 +- .../amethyst/ui/components/InvoicePreview.kt | 47 ++- .../amethyst/ui/components/RichTextViewer.kt | 270 ++++++++++-------- .../amethyst/ui/note/NoteCompose.kt | 39 +-- .../amethyst/ui/note/ReplyInformation.kt | 13 +- 5 files changed, 233 insertions(+), 162 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableWithdrawal.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableWithdrawal.kt index 418e25b45a..51a335c3e0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableWithdrawal.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableWithdrawal.kt @@ -5,10 +5,34 @@ import android.net.Uri import androidx.compose.foundation.text.ClickableText import androidx.compose.material.LocalTextStyle import androidx.compose.material.MaterialTheme -import androidx.compose.runtime.Composable +import androidx.compose.material.Text +import androidx.compose.runtime.* import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextDirection import androidx.core.content.ContextCompat +import com.vitorpamplona.amethyst.service.lnurl.LnWithdrawalUtil +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +@Composable +fun MayBeWithdrawal(lnurlWord: String) { + var lnWithdrawal by remember { mutableStateOf(null) } + + LaunchedEffect(key1 = lnurlWord) { + withContext(Dispatchers.IO) { + lnWithdrawal = LnWithdrawalUtil.findWithdrawal(lnurlWord) + } + } + + lnWithdrawal?.let { + ClickableWithdrawal(withdrawalString = it) + } + ?: Text( + text = "$lnurlWord ", + style = LocalTextStyle.current.copy(textDirection = TextDirection.Content) + ) +} @Composable fun ClickableWithdrawal(withdrawalString: String) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt index f945164000..d86f81c55b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt @@ -9,13 +9,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Divider -import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.runtime.Composable +import androidx.compose.material.* +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -24,22 +19,48 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.content.ContextCompat.startActivity import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.math.BigDecimal import java.text.NumberFormat @Composable -fun InvoicePreview(lnInvoice: String) { - val amount = try { - LnInvoiceUtil.getAmountInSats(lnInvoice) - } catch (e: Exception) { - e.printStackTrace() - null +fun MayBeInvoicePreview(lnbcWord: String) { + var lnInvoice by remember { mutableStateOf?>(null) } + + LaunchedEffect(key1 = lnbcWord) { + withContext(Dispatchers.IO) { + val myInvoice = LnInvoiceUtil.findInvoice(lnbcWord) + if (myInvoice != null) { + val myInvoiceAmount = try { + LnInvoiceUtil.getAmountInSats(myInvoice) + } catch (e: Exception) { + e.printStackTrace() + null + } + + lnInvoice = Pair(myInvoice, myInvoiceAmount) + } + } } + lnInvoice?.let { + InvoicePreview(it.first, it.second) + } + ?: Text( + text = "$lnbcWord ", + style = LocalTextStyle.current.copy(textDirection = TextDirection.Content) + ) +} + +@Composable +fun InvoicePreview(lnInvoice: String, amount: BigDecimal?) { val context = LocalContext.current Column( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index a77a187a8f..fcdaa816a8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -2,7 +2,6 @@ package com.vitorpamplona.amethyst.ui.components import android.util.Log import android.util.Patterns -import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.* @@ -14,7 +13,7 @@ import androidx.compose.material.Icon import androidx.compose.material.LocalTextStyle import androidx.compose.material.MaterialTheme import androidx.compose.material.Text -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color @@ -34,12 +33,15 @@ import com.halilibo.richtext.ui.RichTextStyle import com.halilibo.richtext.ui.material.MaterialRichText import com.halilibo.richtext.ui.resolveDefaults import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.checkForHashtagWithIcon -import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil import com.vitorpamplona.amethyst.service.lnurl.LnWithdrawalUtil import com.vitorpamplona.amethyst.service.nip19.Nip19 import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import java.net.MalformedURLException import java.net.URISyntaxException import java.net.URL @@ -114,7 +116,7 @@ fun RichTextViewer( ) ) - Column(modifier = modifier.animateContentSize()) { + Column(modifier = modifier) { if (content.startsWith("# ") || content.contains("##") || content.contains("**") || @@ -164,25 +166,9 @@ fun RichTextViewer( UrlPreview(word, "$word ") } } else if (word.startsWith("lnbc", true)) { - val lnInvoice = LnInvoiceUtil.findInvoice(word) - if (lnInvoice != null) { - InvoicePreview(lnInvoice) - } else { - Text( - text = "$word ", - style = LocalTextStyle.current.copy(textDirection = TextDirection.Content) - ) - } + MayBeInvoicePreview(word) } else if (word.startsWith("lnurl", true)) { - val lnWithdrawal = LnWithdrawalUtil.findWithdrawal(word) - if (lnWithdrawal != null) { - ClickableWithdrawal(withdrawalString = lnWithdrawal) - } else { - Text( - text = "$word ", - style = LocalTextStyle.current.copy(textDirection = TextDirection.Content) - ) - } + MayBeWithdrawal(word) } else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) { ClickableEmail(word) } else if (word.length > 6 && Patterns.PHONE.matcher(word).matches()) { @@ -304,67 +290,90 @@ fun isBechLink(word: String): Boolean { @Composable fun BechLink(word: String, canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, navController: NavController) { - val nip19Route = Nip19.uriToRoute(word) + var nip19Route by remember { mutableStateOf(null) } + var baseNotePair by remember { mutableStateOf?>(null) } - if (nip19Route == null) { - Text(text = "$word ") - } else { - if (nip19Route.type == Nip19.Type.NOTE || nip19Route.type == Nip19.Type.EVENT || nip19Route.type == Nip19.Type.ADDRESS) { - val note = LocalCache.checkGetOrCreateNote(nip19Route.hex) - if (note != null) { - if (canPreview) { - NoteCompose( - baseNote = note, - accountViewModel = accountViewModel, - modifier = Modifier - .padding(top = 2.dp, bottom = 0.dp, start = 0.dp, end = 0.dp) - .fillMaxWidth() - .clip(shape = RoundedCornerShape(15.dp)) - .border( - 1.dp, - MaterialTheme.colors.onSurface.copy(alpha = 0.12f), - RoundedCornerShape(15.dp) - ), - parentBackgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.05f) - .compositeOver(backgroundColor), - isQuotedNote = true, - navController = navController - ) + LaunchedEffect(key1 = word) { + withContext(Dispatchers.IO) { + Nip19.uriToRoute(word)?.let { + if (it.type == Nip19.Type.NOTE || it.type == Nip19.Type.EVENT || it.type == Nip19.Type.ADDRESS) { + LocalCache.checkGetOrCreateNote(it.hex)?.let { note -> + baseNotePair = Pair(note, it.additionalChars) + } } else { - ClickableRoute(nip19Route, navController) + nip19Route = nip19Route } - } else { - ClickableRoute(nip19Route, navController) } - } else { - ClickableRoute(nip19Route, navController) } } + + if (canPreview) { + baseNotePair?.let { + NoteCompose( + baseNote = it.first, + accountViewModel = accountViewModel, + modifier = Modifier + .padding(top = 2.dp, bottom = 0.dp, start = 0.dp, end = 0.dp) + .fillMaxWidth() + .clip(shape = RoundedCornerShape(15.dp)) + .border( + 1.dp, + MaterialTheme.colors.onSurface.copy(alpha = 0.12f), + RoundedCornerShape(15.dp) + ), + parentBackgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.05f) + .compositeOver(backgroundColor), + isQuotedNote = true, + navController = navController + ) + Text( + "${it.second} " + ) + } ?: nip19Route?.let { + ClickableRoute(it, navController) + } + ?: Text(text = "$word ") + } else { + nip19Route?.let { + ClickableRoute(it, navController) + } + ?: Text(text = "$word ") + } } @Composable fun HashTag(word: String, accountViewModel: AccountViewModel, navController: NavController) { - val hashtagMatcher = hashTagsPattern.matcher(word) + var tagSuffixPair by remember { mutableStateOf?>(null) } - val (tag, suffix) = try { - hashtagMatcher.find() - Pair(hashtagMatcher.group(1), hashtagMatcher.group(2)) - } catch (e: Exception) { - Log.e("Hashtag Parser", "Couldn't link hashtag $word", e) - Pair(null, null) + LaunchedEffect(key1 = word) { + withContext(Dispatchers.IO) { + val hashtagMatcher = hashTagsPattern.matcher(word) + + val (myTag, mySuffix) = try { + hashtagMatcher.find() + Pair(hashtagMatcher.group(1), hashtagMatcher.group(2)) + } catch (e: Exception) { + Log.e("Hashtag Parser", "Couldn't link hashtag $word", e) + Pair(null, null) + } + + if (myTag != null) { + tagSuffixPair = Pair(myTag, mySuffix) + } + } } - if (tag != null) { - val hashtagIcon = checkForHashtagWithIcon(tag) + tagSuffixPair?.let { tagPair -> + val hashtagIcon = checkForHashtagWithIcon(tagPair.first) ClickableText( text = buildAnnotatedString { withStyle( LocalTextStyle.current.copy(color = MaterialTheme.colors.primary).toSpanStyle() ) { - append("#$tag") + append("#${tagPair.first}") } }, - onClick = { navController.navigate("Hashtag/$tag") } + onClick = { navController.navigate("Hashtag/${tagPair.first}") } ) if (hashtagIcon != null) { @@ -387,14 +396,12 @@ fun HashTag(word: String, accountViewModel: AccountViewModel, navController: Nav placeholderVerticalAlign = PlaceholderVerticalAlign.Center ) ) { - if (hashtagIcon != null) { - Icon( - painter = painterResource(hashtagIcon.icon), - contentDescription = hashtagIcon.description, - tint = hashtagIcon.color, - modifier = hashtagIcon.modifier - ) - } + Icon( + painter = painterResource(hashtagIcon.icon), + contentDescription = hashtagIcon.description, + tint = hashtagIcon.color, + modifier = hashtagIcon.modifier + ) } ) ) @@ -405,69 +412,80 @@ fun HashTag(word: String, accountViewModel: AccountViewModel, navController: Nav inlineContent = inlineContent ) } - Text(text = "$suffix ") - } else { - Text(text = "$word ") - } + tagPair.second?.ifBlank { null }?.let { + Text(text = "$it ") + } + } ?: Text(text = "$word ") } @Composable fun TagLink(word: String, tags: List>, canPreview: Boolean, backgroundColor: Color, accountViewModel: AccountViewModel, navController: NavController) { - val matcher = tagIndex.matcher(word) + var baseUserPair by remember { mutableStateOf?>(null) } + var baseNotePair by remember { mutableStateOf?>(null) } - val (index, extraCharacters) = try { - matcher.find() - Pair(matcher.group(1)?.toInt(), matcher.group(2) ?: "") - } catch (e: Exception) { - Log.w("Tag Parser", "Couldn't link tag $word", e) - Pair(null, null) - } - - if (index == null) { - return Text(text = "$word ") - } - - if (index >= 0 && index < tags.size) { - if (tags[index][0] == "p") { - val baseUser = LocalCache.checkGetOrCreateUser(tags[index][1]) - if (baseUser != null) { - ClickableUserTag(baseUser, navController) - Text(text = "$extraCharacters ") - } else { - // if here the tag is not a valid Nostr Hex - Text(text = "$word ") + LaunchedEffect(key1 = word) { + withContext(Dispatchers.IO) { + val matcher = tagIndex.matcher(word) + val (index, suffix) = try { + matcher.find() + Pair(matcher.group(1)?.toInt(), matcher.group(2) ?: "") + } catch (e: Exception) { + Log.w("Tag Parser", "Couldn't link tag $word", e) + Pair(null, null) } - } else if (tags[index][0] == "e") { - val note = LocalCache.checkGetOrCreateNote(tags[index][1]) - if (note != null) { - if (canPreview) { - NoteCompose( - baseNote = note, - accountViewModel = accountViewModel, - modifier = Modifier - .padding(top = 2.dp, bottom = 0.dp, start = 0.dp, end = 0.dp) - .fillMaxWidth() - .clip(shape = RoundedCornerShape(15.dp)) - .border( - 1.dp, - MaterialTheme.colors.onSurface.copy(alpha = 0.12f), - RoundedCornerShape(15.dp) - ), - parentBackgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.05f) - .compositeOver(backgroundColor), - isQuotedNote = true, - navController = navController - ) - } else { - ClickableNoteTag(note, navController) - Text(text = "$extraCharacters ") + + if (index != null && index >= 0 && index < tags.size) { + val tag = tags[index] + + if (tag.size > 1) { + if (tag[0] == "p") { + LocalCache.checkGetOrCreateUser(tags[index][1])?.let { + baseUserPair = Pair(it, suffix) + } + } else if (tag[0] == "e" || tag[0] == "a") { + LocalCache.checkGetOrCreateNote(tags[index][1])?.let { + baseNotePair = Pair(it, suffix) + } + } } - } else { - // if here the tag is not a valid Nostr Hex - Text(text = "$word ") } - } else { - Text(text = "$word ") } } + + baseUserPair?.let { + ClickableUserTag(it.first, navController) + Text(text = "${it.second} ") + } + + baseNotePair?.let { + if (canPreview) { + NoteCompose( + baseNote = it.first, + accountViewModel = accountViewModel, + modifier = Modifier + .padding(top = 2.dp, bottom = 0.dp, start = 0.dp, end = 0.dp) + .fillMaxWidth() + .clip(shape = RoundedCornerShape(15.dp)) + .border( + 1.dp, + MaterialTheme.colors.onSurface.copy(alpha = 0.12f), + RoundedCornerShape(15.dp) + ), + parentBackgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.05f) + .compositeOver(backgroundColor), + isQuotedNote = true, + navController = navController + ) + it.second?.ifBlank { null }?.let { + Text(text = "$it ") + } + } else { + ClickableNoteTag(it.first, navController) + Text(text = "${it.second} ") + } + } + + if (baseNotePair == null && baseUserPair == null) { + Text(text = "$word ") + } } 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 7d299b8279..8aef733003 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 @@ -65,7 +65,7 @@ import kotlin.math.ceil import kotlin.time.ExperimentalTime import kotlin.time.measureTimedValue -@OptIn(ExperimentalFoundationApi::class, ExperimentalTime::class) +@OptIn(ExperimentalTime::class) @Composable fun NoteCompose( baseNote: Note, @@ -130,6 +130,19 @@ fun NoteComposeInner( var moreActionsExpanded by remember { mutableStateOf(false) } + var isAcceptable by remember { mutableStateOf(true) } + var canPreview by remember { mutableStateOf(true) } + + LaunchedEffect(key1 = noteReportsState) { + withContext(Dispatchers.IO) { + canPreview = note?.author === account.userProfile() || + (note?.author?.let { account.userProfile().isFollowingCached(it) } ?: true) || + !noteForReports.hasAnyReports() + + isAcceptable = account.isAcceptable(noteForReports) + } + } + val noteEvent = note?.event val baseChannel = note?.channel() @@ -141,7 +154,7 @@ fun NoteComposeInner( ), isBoostedNote ) - } else if (!account.isAcceptable(noteForReports) && !showHiddenNote) { + } else if (!isAcceptable && !showHiddenNote) { if (!account.isHidden(noteForReports.author!!)) { HiddenNote( account.getRelevantReports(noteForReports), @@ -343,11 +356,6 @@ fun NoteComposeInner( Spacer(modifier = Modifier.height(3.dp)) if (!makeItShort && noteEvent is TextNoteEvent && (note.replyTo != null || noteEvent.mentions().isNotEmpty())) { - val sortedMentions = noteEvent.mentions() - .mapNotNull { LocalCache.checkGetOrCreateUser(it) } - .toSet() - .sortedBy { account.userProfile().isFollowingCached(it) } - val replyingDirectlyTo = note.replyTo?.lastOrNull() if (replyingDirectlyTo != null && unPackReply) { NoteCompose( @@ -369,7 +377,7 @@ fun NoteComposeInner( navController = navController ) } else { - ReplyInformation(note.replyTo, sortedMentions, account, navController) + ReplyInformation(note.replyTo, noteEvent.mentions(), account, navController) } Spacer(modifier = Modifier.height(5.dp)) } else if (!makeItShort && noteEvent is ChannelMessageEvent && (note.replyTo != null || noteEvent.mentions().isNotEmpty())) { @@ -501,10 +509,6 @@ fun NoteComposeInner( } else { val eventContent = accountViewModel.decrypt(note) - val canPreview = note.author == account.userProfile() || - (note.author?.let { account.userProfile().isFollowingCached(it) } ?: true) || - !noteForReports.hasAnyReports() - if (eventContent != null) { if (makeItShort && note.author == account.userProfile()) { Text( @@ -524,7 +528,7 @@ fun NoteComposeInner( navController ) - DisplayUncitedHashtags(noteEvent, eventContent, navController) + DisplayUncitedHashtags(noteEvent.hashtags(), eventContent, navController) } if (noteEvent is PollNoteEvent) { @@ -582,11 +586,10 @@ fun DisplayFollowingHashtagsInPost( @Composable fun DisplayUncitedHashtags( - noteEvent: EventInterface, + hashtags: List, eventContent: String, navController: NavController ) { - val hashtags = noteEvent.hashtags() if (hashtags.isNotEmpty()) { FlowRow( modifier = Modifier.padding(top = 5.dp) @@ -868,7 +871,11 @@ private fun RelayBadges(baseNote: Note) { items(relaysToDisplay.size) { val url = relaysToDisplay[it].removePrefix("wss://").removePrefix("ws://") - Box(Modifier.padding(1.dp).size(15.dp)) { + Box( + Modifier + .padding(1.dp) + .size(15.dp) + ) { RobohashFallbackAsyncImage( robot = "https://$url/favicon.ico", model = "https://$url/favicon.ico", diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt index 72c92184e8..e755a8df17 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt @@ -16,14 +16,15 @@ import androidx.compose.ui.unit.sp import androidx.navigation.NavController import com.google.accompanist.flowlayout.FlowRow import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.Channel -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.* @Composable -fun ReplyInformation(replyTo: List?, mentions: List?, account: Account, navController: NavController) { - ReplyInformation(replyTo, mentions, account) { +fun ReplyInformation(replyTo: List?, mentions: List, account: Account, navController: NavController) { + val sortedMentions = mentions.mapNotNull { LocalCache.checkGetOrCreateUser(it) } + .toSet() + .sortedBy { account.userProfile().isFollowingCached(it) } + + ReplyInformation(replyTo, sortedMentions, account) { navController.navigate("User/${it.pubkeyHex}") } } From 97490bb6ef68ada0303255723c6fee1ed18f5b95 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 7 Apr 2023 17:19:47 -0400 Subject: [PATCH 095/145] Using bitcoin icon instead of dollar. --- .../com/vitorpamplona/amethyst/ui/actions/NewPostView.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index 613af9d3d7..d65c5980d1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -13,7 +13,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.MonetizationOn +import androidx.compose.material.icons.filled.CurrencyBitcoin import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember @@ -45,6 +45,7 @@ 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 com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import kotlinx.coroutines.delay @OptIn(ExperimentalComposeUiApi::class) @@ -327,17 +328,17 @@ private fun AddLnInvoiceButton( ) { if (!isLnInvoiceActive) { Icon( - imageVector = Icons.Default.MonetizationOn, + imageVector = Icons.Default.CurrencyBitcoin, null, modifier = Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground ) } else { Icon( - imageVector = Icons.Default.MonetizationOn, + imageVector = Icons.Default.CurrencyBitcoin, null, modifier = Modifier.size(20.dp), - tint = Color.Green.copy(alpha = 0.52f) + tint = BitcoinOrange ) } } From 03b76357ff423d69a39a48c0260c3e4fcd73cf38 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 7 Apr 2023 17:20:18 -0400 Subject: [PATCH 096/145] Migrates poll and invoice option availability to a mutable state --- .../amethyst/ui/actions/NewPostView.kt | 4 ++-- .../amethyst/ui/actions/NewPostViewModel.kt | 13 +++++-------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index d65c5980d1..82175a9130 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -270,13 +270,13 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n postViewModel.upload(it, context) } - if (postViewModel.canUsePoll()) { + if (postViewModel.canUsePoll) { AddPollButton(postViewModel.wantsPoll) { postViewModel.wantsPoll = !postViewModel.wantsPoll } } - if (postViewModel.canAddLnInvoice()) { + if (postViewModel.canAddInvoice) { AddLnInvoiceButton(postViewModel.wantsInvoice) { postViewModel.wantsInvoice = !postViewModel.wantsInvoice } 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 5c0a13b167..5765faf053 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 @@ -38,6 +38,7 @@ open class NewPostViewModel : ViewModel() { var userSuggestionAnchor: TextRange? = null // Polls + var canUsePoll by mutableStateOf(false) var wantsPoll by mutableStateOf(false) var zapRecipients = mutableStateListOf() var pollOptions = newStateMapPollOptions() @@ -53,6 +54,7 @@ open class NewPostViewModel : ViewModel() { var isValidClosedAt = mutableStateOf(true) // Invoices + var canAddInvoice by mutableStateOf(false) var wantsInvoice by mutableStateOf(false) open fun load(account: Account, replyingTo: Note?, quote: Note?) { @@ -79,6 +81,9 @@ open class NewPostViewModel : ViewModel() { message = TextFieldValue(message.text + "\n\n@${it.idNote()}") } + canAddInvoice = account.userProfile().info?.lnAddress() != null + canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channel() == null + this.account = account } @@ -190,12 +195,4 @@ open class NewPostViewModel : ViewModel() { return message.text.isNotBlank() && !isUploadingImage && !wantsInvoice && (!wantsPoll || pollOptions.values.all { it.isNotEmpty() }) } - - fun canUsePoll(): Boolean { - return originalNote?.event !is PrivateDmEvent && originalNote?.channel() == null - } - - fun canAddLnInvoice(): Boolean { - return account?.userProfile()?.info?.lnAddress() != null - } } From 49ed594f53d36c1fe97df0b6a401668cbd65b77f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 7 Apr 2023 17:21:21 -0400 Subject: [PATCH 097/145] Caches logged user in Note compose --- .../amethyst/ui/note/NoteCompose.kt | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 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 8aef733003..f4cdac619e 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 @@ -116,6 +116,7 @@ fun NoteComposeInner( ) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account ?: return + val loggedIn = account.userProfile() val noteState by baseNote.live().metadata.observeAsState() val note = noteState?.note @@ -135,8 +136,8 @@ fun NoteComposeInner( LaunchedEffect(key1 = noteReportsState) { withContext(Dispatchers.IO) { - canPreview = note?.author === account.userProfile() || - (note?.author?.let { account.userProfile().isFollowingCached(it) } ?: true) || + canPreview = note?.author === loggedIn || + (note?.author?.let { loggedIn.isFollowingCached(it) } ?: true) || !noteForReports.hasAnyReports() isAcceptable = account.isAcceptable(noteForReports) @@ -158,7 +159,7 @@ fun NoteComposeInner( if (!account.isHidden(noteForReports.author!!)) { HiddenNote( account.getRelevantReports(noteForReports), - account.userProfile(), + loggedIn, modifier, isBoostedNote, navController, @@ -231,7 +232,7 @@ fun NoteComposeInner( .width(55.dp) .padding(0.dp) ) { - NoteAuthorPicture(note, navController, account.userProfile(), 55.dp) + NoteAuthorPicture(note, navController, loggedIn, 55.dp) if (noteEvent is RepostEvent) { note.replyTo?.lastOrNull()?.let { @@ -244,7 +245,7 @@ fun NoteComposeInner( NoteAuthorPicture( it, navController, - account.userProfile(), + loggedIn, 35.dp, pictureModifier = Modifier.border(2.dp, MaterialTheme.colors.background, CircleShape) ) @@ -299,7 +300,7 @@ fun NoteComposeInner( ) { Row(verticalAlignment = Alignment.CenterVertically) { if (isQuotedNote) { - NoteAuthorPicture(note, navController, account.userProfile(), 25.dp) + NoteAuthorPicture(note, navController, loggedIn, 25.dp) Spacer(Modifier.padding(horizontal = 5.dp)) NoteUsernameDisplay(note, Modifier.weight(1f)) } else { @@ -384,7 +385,7 @@ fun NoteComposeInner( val sortedMentions = noteEvent.mentions() .mapNotNull { LocalCache.checkGetOrCreateUser(it) } .toSet() - .sortedBy { account.userProfile().isFollowingCached(it) } + .sortedBy { loggedIn.isFollowingCached(it) } note.channel()?.let { ReplyInformationChannel(note.replyTo, sortedMentions, it, navController) @@ -435,7 +436,7 @@ fun NoteComposeInner( thickness = 0.25.dp ) } else if (noteEvent is LongTextNoteEvent) { - LongFormHeader(noteEvent, note, account.userProfile()) + LongFormHeader(noteEvent, note, loggedIn) ReactionsRow(note, accountViewModel) @@ -453,7 +454,7 @@ fun NoteComposeInner( UserPicture( user = it, navController = navController, - userAccount = account.userProfile(), + userAccount = loggedIn, size = 35.dp ) } @@ -479,8 +480,8 @@ fun NoteComposeInner( thickness = 0.25.dp ) } else if (noteEvent is PrivateDmEvent && - noteEvent.recipientPubKey() != account.userProfile().pubkeyHex && - note.author != account.userProfile() + noteEvent.recipientPubKey() != loggedIn.pubkeyHex && + note.author !== loggedIn ) { val recepient = noteEvent.recipientPubKey()?.let { LocalCache.checkGetOrCreateUser(it) } @@ -510,7 +511,7 @@ fun NoteComposeInner( val eventContent = accountViewModel.decrypt(note) if (eventContent != null) { - if (makeItShort && note.author == account.userProfile()) { + if (makeItShort && note.author == loggedIn) { Text( text = eventContent, color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f), From 241372ce61ad5504e74322e3ac045ed8e3711905 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 7 Apr 2023 17:37:07 -0400 Subject: [PATCH 098/145] Adds DMs to the id search filter --- .../amethyst/service/NostrSingleEventDataSource.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 20b3533ef0..29f6cf38cd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleEventDataSource.kt @@ -118,7 +118,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") { 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, - PollNoteEvent.kind + PollNoteEvent.kind, PrivateDmEvent.kind ), ids = interestedEvents.toList() ) From c58b13e2f7386c76f671bb51c6a622a887eaf863 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 7 Apr 2023 17:37:25 -0400 Subject: [PATCH 099/145] Simplifies modifiers --- .../main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f4cdac619e..7e1674f67f 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 @@ -324,7 +324,7 @@ fun NoteComposeInner( ) IconButton( - modifier = Modifier.then(Modifier.size(24.dp)), + modifier = Modifier.size(24.dp), onClick = { moreActionsExpanded = true } ) { Icon( From e6554a5b4ae45f0a2c1e68256f44d39af9f0c7cd Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 7 Apr 2023 17:38:02 -0400 Subject: [PATCH 100/145] Moves tags calculation to Launched Effect --- .../com/vitorpamplona/amethyst/ui/note/NoteCompose.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 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 7e1674f67f..91021b376e 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 @@ -566,11 +566,15 @@ fun DisplayFollowingHashtagsInPost( account: Account, navController: NavController ) { + var firstTag by remember { mutableStateOf(null) } + + LaunchedEffect(key1 = noteEvent) { + firstTag = noteEvent.firstIsTaggedHashes(account.followingTagSet()) + } + Column() { Row(verticalAlignment = Alignment.CenterVertically) { - val firstTag = - noteEvent.firstIsTaggedHashes(account.followingTagSet()) - if (firstTag != null) { + firstTag?.let { ClickableText( text = AnnotatedString(" #$firstTag"), onClick = { navController.navigate("Hashtag/$firstTag") }, From b89efb547e3faf668104396155ad38b2d7e2fce6 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 7 Apr 2023 17:38:25 -0400 Subject: [PATCH 101/145] Correctly points Users to the right DM when clicking in the notification. --- .../vitorpamplona/amethyst/ui/note/NoteCompose.kt | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) 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 91021b376e..dc13ec0ab9 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 @@ -207,7 +207,20 @@ fun NoteComposeInner( navController.navigate("Channel/${it.idHex}") } } else if (noteEvent is PrivateDmEvent) { - navController.navigate("Room/${note.author?.pubkeyHex}") + val replyAuthorBase = + (note.event as? PrivateDmEvent) + ?.recipientPubKey() + ?.let { LocalCache.getOrCreateUser(it) } + + var userToComposeOn = note.author!! + + if (replyAuthorBase != null) { + if (note.author == accountViewModel.userProfile()) { + userToComposeOn = replyAuthorBase + } + } + + navController.navigate("Room/${userToComposeOn.pubkeyHex}") } else { navController.navigate("Note/${note.idHex}") } From b01df2b38eed13e8328a882e00dff5bd392cd267 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 7 Apr 2023 17:41:00 -0400 Subject: [PATCH 102/145] Fixes MultiSet Compose Routes --- .../amethyst/ui/note/MultiSetCompose.kt | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index 2f262e9d4b..b001cce847 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -33,9 +33,11 @@ import com.google.accompanist.flowlayout.FlowRow import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent +import com.vitorpamplona.amethyst.service.model.PrivateDmEvent import com.vitorpamplona.amethyst.ui.screen.MultiSetCard import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange @@ -78,14 +80,29 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun .background(backgroundColor) .combinedClickable( onClick = { - if (noteEvent !is ChannelMessageEvent) { - navController.navigate("Note/${note.idHex}") { - launchSingleTop = true - } - } else { + if (noteEvent is ChannelMessageEvent) { note.channel()?.let { navController.navigate("Channel/${it.idHex}") } + } else if (noteEvent is PrivateDmEvent) { + val replyAuthorBase = + (note.event as? PrivateDmEvent) + ?.recipientPubKey() + ?.let { LocalCache.getOrCreateUser(it) } + + var userToComposeOn = note.author!! + + if (replyAuthorBase != null) { + if (note.author == accountViewModel.userProfile()) { + userToComposeOn = replyAuthorBase + } + } + + navController.navigate("Room/${userToComposeOn.pubkeyHex}") + } else { + navController.navigate("Note/${note.idHex}") { + launchSingleTop = true + } } }, onLongClick = { popupExpanded = true } From 42b89f438b82a79d75cd87a233aeed6fdc43b526 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 7 Apr 2023 17:44:40 -0400 Subject: [PATCH 103/145] Only calculates follows when needed. --- .../vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt | 5 +++-- .../amethyst/ui/screen/loggedIn/AccountViewModel.kt | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt index b6a8a54964..ca98d7048b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt @@ -109,8 +109,6 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni var showDeleteAlertDialog by remember { mutableStateOf(false) } var showBlockAlertDialog by remember { mutableStateOf(false) } var showReportDialog by remember { mutableStateOf(false) } - val isOwnNote = accountViewModel.isLoggedUser(note.author) - val isFollowingUser = !isOwnNote && accountViewModel.isFollowing(note.author!!) val backgroundColor = if (MaterialTheme.colors.isLight) { MaterialTheme.colors.primary @@ -129,6 +127,9 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni } if (popupExpanded) { + val isOwnNote = accountViewModel.isLoggedUser(note.author) + val isFollowingUser = !isOwnNote && accountViewModel.isFollowing(note.author!!) + Popup(onDismissRequest = onDismiss) { Card( modifier = Modifier.shadow(elevation = 6.dp, shape = cardShape), 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 73330c6900..e9f777be1f 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 @@ -170,7 +170,7 @@ class AccountViewModel(private val account: Account) : ViewModel() { } fun isLoggedUser(user: User?): Boolean { - return account.userProfile() == user + return account.userProfile() === user } fun isFollowing(user: User?): Boolean { From ef567110d0edb75903037593c37df1dbbdf42b93 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 7 Apr 2023 18:02:54 -0400 Subject: [PATCH 104/145] Moving processing flags to Launched Effect. --- .../amethyst/ui/note/NoteCompose.kt | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 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 dc13ec0ab9..be5075c253 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 @@ -1087,6 +1087,13 @@ fun UserPicture( } } +data class DropDownParams( + val isFollowingAuthor: Boolean, + val isPrivateBookmarkNote: Boolean, + val isPublicBookmarkNote: Boolean, + val isLoggedUser: Boolean +) + @Composable fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, accountViewModel: AccountViewModel) { val clipboardManager = LocalClipboardManager.current @@ -1094,11 +1101,26 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, val actContext = LocalContext.current var reportDialogShowing by remember { mutableStateOf(false) } + var state by remember { + mutableStateOf( + DropDownParams(false, false, false, false) + ) + } + + LaunchedEffect(key1 = note) { + state = DropDownParams( + accountViewModel.isFollowing(note.author), + accountViewModel.isInPrivateBookmarks(note), + accountViewModel.isInPublicBookmarks(note), + accountViewModel.isLoggedUser(note.author) + ) + } + DropdownMenu( expanded = popupExpanded, onDismissRequest = onDismiss ) { - if (!accountViewModel.isFollowing(note.author)) { + if (!state.isFollowingAuthor) { DropdownMenuItem(onClick = { accountViewModel.follow( note.author ?: return@DropdownMenuItem @@ -1135,7 +1157,7 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, Text(stringResource(R.string.quick_action_share)) } Divider() - if (accountViewModel.isInPrivateBookmarks(note)) { + if (state.isPrivateBookmarkNote) { DropdownMenuItem(onClick = { accountViewModel.removePrivateBookmark(note); onDismiss() }) { Text(stringResource(R.string.remove_from_private_bookmarks)) } @@ -1144,7 +1166,7 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, Text(stringResource(R.string.add_to_private_bookmarks)) } } - if (accountViewModel.isInPublicBookmarks(note)) { + if (state.isPublicBookmarkNote) { DropdownMenuItem(onClick = { accountViewModel.removePublicBookmark(note); onDismiss() }) { Text(stringResource(R.string.remove_from_public_bookmarks)) } @@ -1158,7 +1180,7 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, Text(stringResource(R.string.broadcast)) } Divider() - if (accountViewModel.isLoggedUser(note.author)) { + if (state.isLoggedUser) { DropdownMenuItem(onClick = { accountViewModel.delete(note); onDismiss() }) { Text(stringResource(R.string.request_deletion)) } From 234751a4b34c08b6ec7aca125a30925d6671f66f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 7 Apr 2023 18:11:50 -0400 Subject: [PATCH 105/145] Avoids loading markdown style when it won't be used. --- .../amethyst/ui/components/RichTextViewer.kt | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index fcdaa816a8..ed63f83c3a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -82,40 +82,6 @@ fun RichTextViewer( accountViewModel: AccountViewModel, navController: NavController ) { - val myMarkDownStyle = richTextDefaults.copy( - codeBlockStyle = richTextDefaults.codeBlockStyle?.copy( - textStyle = TextStyle( - fontFamily = FontFamily.Monospace, - fontSize = 14.sp - ), - modifier = Modifier - .padding(0.dp) - .fillMaxWidth() - .clip(shape = RoundedCornerShape(15.dp)) - .border( - 1.dp, - MaterialTheme.colors.onSurface.copy(alpha = 0.12f), - RoundedCornerShape(15.dp) - ) - .background( - MaterialTheme.colors.onSurface - .copy(alpha = 0.05f) - .compositeOver(backgroundColor) - ) - ), - stringStyle = richTextDefaults.stringStyle?.copy( - linkStyle = SpanStyle( - textDecoration = TextDecoration.Underline, - color = MaterialTheme.colors.primary - ), - codeStyle = SpanStyle( - fontFamily = FontFamily.Monospace, - fontSize = 14.sp, - background = MaterialTheme.colors.onSurface.copy(alpha = 0.22f).compositeOver(backgroundColor) - ) - ) - ) - Column(modifier = modifier) { if (content.startsWith("# ") || content.contains("##") || @@ -123,6 +89,40 @@ fun RichTextViewer( content.contains("__") || content.contains("```") ) { + val myMarkDownStyle = richTextDefaults.copy( + codeBlockStyle = richTextDefaults.codeBlockStyle?.copy( + textStyle = TextStyle( + fontFamily = FontFamily.Monospace, + fontSize = 14.sp + ), + modifier = Modifier + .padding(0.dp) + .fillMaxWidth() + .clip(shape = RoundedCornerShape(15.dp)) + .border( + 1.dp, + MaterialTheme.colors.onSurface.copy(alpha = 0.12f), + RoundedCornerShape(15.dp) + ) + .background( + MaterialTheme.colors.onSurface + .copy(alpha = 0.05f) + .compositeOver(backgroundColor) + ) + ), + stringStyle = richTextDefaults.stringStyle?.copy( + linkStyle = SpanStyle( + textDecoration = TextDecoration.Underline, + color = MaterialTheme.colors.primary + ), + codeStyle = SpanStyle( + fontFamily = FontFamily.Monospace, + fontSize = 14.sp, + background = MaterialTheme.colors.onSurface.copy(alpha = 0.22f).compositeOver(backgroundColor) + ) + ) + ) + MaterialRichText( style = myMarkDownStyle ) { @@ -301,7 +301,7 @@ fun BechLink(word: String, canPreview: Boolean, backgroundColor: Color, accountV baseNotePair = Pair(note, it.additionalChars) } } else { - nip19Route = nip19Route + nip19Route = it } } } From 80e58f72f8d3f3203979befe1040198b0bb73e20 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 7 Apr 2023 18:26:13 -0400 Subject: [PATCH 106/145] Fixes nprofile1 tags --- .../amethyst/ui/components/RichTextViewer.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index ed63f83c3a..814de2ea20 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -283,7 +283,7 @@ private fun isArabic(text: String): Boolean { } fun isBechLink(word: String): Boolean { - val cleaned = word.lowercase().removePrefix("@").removePrefix("nostr:").removePrefix("@").take(7) + val cleaned = word.lowercase().removePrefix("@").removePrefix("nostr:").removePrefix("@") return listOf("npub1", "naddr1", "note1", "nprofile1", "nevent1").any { cleaned.startsWith(it) } } @@ -331,13 +331,11 @@ fun BechLink(word: String, canPreview: Boolean, backgroundColor: Color, accountV ) } ?: nip19Route?.let { ClickableRoute(it, navController) - } - ?: Text(text = "$word ") + } ?: Text(text = "$word ") } else { nip19Route?.let { ClickableRoute(it, navController) - } - ?: Text(text = "$word ") + } ?: Text(text = "$word ") } } @@ -412,10 +410,11 @@ fun HashTag(word: String, accountViewModel: AccountViewModel, navController: Nav inlineContent = inlineContent ) } - tagPair.second?.ifBlank { null }?.let { + tagPair.second?.ifBlank { "" }?.let { Text(text = "$it ") } } ?: Text(text = "$word ") + } @Composable From 43497f2ce0b9999f6d6f01d9edbaf21fdcf464c1 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 7 Apr 2023 18:26:19 -0400 Subject: [PATCH 107/145] fixes format --- .../com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 814de2ea20..cf3299873b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -414,7 +414,6 @@ fun HashTag(word: String, accountViewModel: AccountViewModel, navController: Nav Text(text = "$it ") } } ?: Text(text = "$word ") - } @Composable From ed221559401c9fa0da456ff64f4259e2009e91fd Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 7 Apr 2023 18:30:34 -0400 Subject: [PATCH 108/145] Simplifying tag structure --- .../vitorpamplona/amethyst/ui/components/RichTextViewer.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index cf3299873b..474a1fea70 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -437,11 +437,11 @@ fun TagLink(word: String, tags: List>, canPreview: Boolean, backgro if (tag.size > 1) { if (tag[0] == "p") { - LocalCache.checkGetOrCreateUser(tags[index][1])?.let { + LocalCache.checkGetOrCreateUser(tag[1])?.let { baseUserPair = Pair(it, suffix) } } else if (tag[0] == "e" || tag[0] == "a") { - LocalCache.checkGetOrCreateNote(tags[index][1])?.let { + LocalCache.checkGetOrCreateNote(tag[1])?.let { baseNotePair = Pair(it, suffix) } } From cea9cdcd344fa52538697917098f2239057dd41e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 7 Apr 2023 18:33:36 -0400 Subject: [PATCH 109/145] v0.32.3 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index cf6a1ba851..fd5da4e0d3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 33 - versionCode 117 - versionName "0.32.2" + versionCode 118 + versionName "0.32.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From 13579f947396e5f38262fc2239ea72673a0c826a Mon Sep 17 00:00:00 2001 From: toadlyBroodle Date: Sun, 9 Apr 2023 11:26:02 +0900 Subject: [PATCH 110/145] add #zappoll to new poll messages --- .../amethyst/ui/actions/NewPostView.kt | 2 ++ .../amethyst/ui/actions/NewPostViewModel.kt | 13 +++++++++++++ app/src/main/res/values/strings.xml | 1 + 3 files changed, 16 insertions(+) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index 82175a9130..213573b225 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -271,8 +271,10 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n } if (postViewModel.canUsePoll) { + val hashtag = stringResource(R.string.poll_hashtag) AddPollButton(postViewModel.wantsPoll) { postViewModel.wantsPoll = !postViewModel.wantsPoll + postViewModel.includePollHashtagInMessage(postViewModel.wantsPoll, hashtag) } } 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 5765faf053..1a98c4e970 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 @@ -195,4 +195,17 @@ open class NewPostViewModel : ViewModel() { return message.text.isNotBlank() && !isUploadingImage && !wantsInvoice && (!wantsPoll || pollOptions.values.all { it.isNotEmpty() }) } + + fun includePollHashtagInMessage(include: Boolean, hashtag: String) { + if (include) { + updateMessage(TextFieldValue(message.text + " $hashtag")) + } else { + updateMessage( + TextFieldValue( + message.text.replace(" $hashtag", "") + .replace(hashtag, "") + ) + ) + } + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 60375a15a8..f1878907d7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -291,4 +291,5 @@ Create and Add Poll authors can\'t vote in their own polls. + #zappoll From 5abbd07722106c4c34039677ba1306895fff727b Mon Sep 17 00:00:00 2001 From: Believethehype Date: Sun, 9 Apr 2023 23:37:23 +0200 Subject: [PATCH 111/145] introduce zap types, implemented non-zaps in custom zap dialog Non-Zaps allow to tip the author of a note without using the zap mechanism (no record on the note) --- .../amethyst/service/model/LnZapEvent.kt | 7 ++++ .../amethyst/ui/note/PollNote.kt | 16 +++++--- .../amethyst/ui/note/ReactionsRow.kt | 10 +++-- .../amethyst/ui/note/ZapCustomDialog.kt | 38 ++++++++++++++----- .../ui/screen/loggedIn/AccountViewModel.kt | 12 ++++-- 5 files changed, 62 insertions(+), 21 deletions(-) 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 ad83690e91..ca8eedee6d 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 @@ -69,4 +69,11 @@ class LnZapEvent( companion object { const val kind = 9735 } + + enum class ZapType() { + PUBLIC, + PRIVATE, // not implemented + ANONYMOUS, // not implemented + NONZAP + } } 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 e46645a34e..6a56c847fe 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 @@ -31,6 +31,7 @@ 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.LnZapEvent import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange @@ -251,7 +252,8 @@ fun ZapVote( scope.launch(Dispatchers.Main) { zappingProgress = it } - } + }, + type = LnZapEvent.ZapType.PUBLIC ) } } else { @@ -378,7 +380,8 @@ fun FilteredZapAmountChoicePopup( zapMessage, context, onError, - onProgress + onProgress, + LnZapEvent.ZapType.PUBLIC ) onDismiss() } @@ -403,7 +406,8 @@ fun FilteredZapAmountChoicePopup( zapMessage, context, onError, - onProgress + onProgress, + LnZapEvent.ZapType.PUBLIC ) onDismiss() } @@ -495,7 +499,8 @@ fun ZapVoteAmountChoicePopup( "", context, onError, - onProgress + onProgress, + LnZapEvent.ZapType.PUBLIC ) onDismiss() } @@ -520,7 +525,8 @@ fun ZapVoteAmountChoicePopup( "", context, onError, - onProgress + onProgress, + LnZapEvent.ZapType.PUBLIC ) onDismiss() } 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 e90641a386..77e7ddfeee 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 @@ -55,6 +55,7 @@ import coil.request.CachePolicy import coil.request.ImageRequest import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.model.LnZapEvent import com.vitorpamplona.amethyst.ui.actions.NewPostView import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange @@ -363,7 +364,8 @@ fun ZapReaction( scope.launch(Dispatchers.Main) { zappingProgress = it } - } + }, + type = LnZapEvent.ZapType.PUBLIC ) } } else if (account.zapAmountChoices.size > 1) { @@ -559,7 +561,8 @@ fun ZapAmountChoicePopup( zapMessage, context, onError, - onProgress + onProgress, + LnZapEvent.ZapType.PUBLIC ) onDismiss() } @@ -584,7 +587,8 @@ fun ZapAmountChoicePopup( zapMessage, context, onError, - onProgress + onProgress, + LnZapEvent.ZapType.PUBLIC ) onDismiss() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt index b0ea32a753..2fcde04b35 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt @@ -5,12 +5,7 @@ import androidx.compose.foundation.layout.* 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.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -27,15 +22,16 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.model.LnZapEvent import com.vitorpamplona.amethyst.ui.actions.CloseButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch class ZapOptionstViewModel : ViewModel() { private var account: Account? = null - - var customAmount by mutableStateOf(TextFieldValue("1000")) + var customAmount by mutableStateOf(TextFieldValue("21")) var customMessage by mutableStateOf(TextFieldValue("")) fun load(account: Account) { @@ -63,11 +59,21 @@ fun ZapCustomDialog(onClose: () -> Unit, account: Account, accountViewModel: Acc val context = LocalContext.current val scope = rememberCoroutineScope() val postViewModel: ZapOptionstViewModel = viewModel() - + postViewModel.load(account) LaunchedEffect(account) { postViewModel.load(account) } + var zappingProgress by remember { mutableStateOf(0f) } + + val zapTypes = listOf( + Pair(LnZapEvent.ZapType.PUBLIC, "Public"), + Pair(LnZapEvent.ZapType.NONZAP, "Non-Zap") + ) + + val zapOptions = zapTypes.map { it.second } + var selectedZapType by remember { mutableStateOf(zapTypes[0]) } + Dialog( onDismissRequest = { onClose() }, properties = DialogProperties( @@ -98,6 +104,7 @@ fun ZapCustomDialog(onClose: () -> Unit, account: Account, accountViewModel: Acc postViewModel.customMessage.text, context, onError = { + zappingProgress = 0f scope.launch { Toast .makeText(context, it, Toast.LENGTH_SHORT).show() @@ -105,8 +112,10 @@ fun ZapCustomDialog(onClose: () -> Unit, account: Account, accountViewModel: Acc }, onProgress = { scope.launch(Dispatchers.Main) { + zappingProgress = it } - } + }, + type = selectedZapType.first ) } onClose() @@ -174,6 +183,15 @@ fun ZapCustomDialog(onClose: () -> Unit, account: Account, accountViewModel: Acc .weight(1f) ) } + TextSpinner( + label = "Zap Type", + placeholder = "Public", + options = zapOptions, + onSelect = { + selectedZapType = zapTypes[it] + }, + modifier = Modifier.fillMaxWidth() + ) } } } 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 e9f777be1f..513c5b1505 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 @@ -14,6 +14,7 @@ import com.vitorpamplona.amethyst.model.AccountState import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver +import com.vitorpamplona.amethyst.service.model.LnZapEvent import com.vitorpamplona.amethyst.service.model.ReportEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -52,7 +53,7 @@ class AccountViewModel(private val account: Account) : ViewModel() { account.delete(account.boostsTo(note)) } - fun zap(note: Note, amount: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit) { + fun zap(note: Note, amount: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit, type: LnZapEvent.ZapType) { val lud16 = note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim() if (lud16.isNullOrBlank()) { @@ -60,7 +61,12 @@ class AccountViewModel(private val account: Account) : ViewModel() { return } - val zapRequest = account.createZapRequestFor(note, pollOption, message) + var zapRequest = account.createZapRequestFor(note, pollOption, message) + var zapRequestJson = zapRequest?.toJson() + + if (type == LnZapEvent.ZapType.NONZAP) { + zapRequestJson = "" + } onProgress(0.10f) @@ -68,7 +74,7 @@ class AccountViewModel(private val account: Account) : ViewModel() { lud16, amount, message, - zapRequest?.toJson(), + zapRequestJson, onSuccess = { onProgress(0.7f) if (account.hasWalletConnectSetup()) { From 9a5d8522fb3f5905d911a54b409162f193f70adb Mon Sep 17 00:00:00 2001 From: Believethehype Date: Mon, 10 Apr 2023 13:25:09 +0200 Subject: [PATCH 112/145] added anonymous zaps --- .../vitorpamplona/amethyst/model/Account.kt | 10 +++++--- .../service/model/LnZapRequestEvent.kt | 25 ++++++++++++++----- .../amethyst/ui/components/InvoiceRequest.kt | 3 ++- .../amethyst/ui/note/PollNote.kt | 2 +- .../amethyst/ui/note/ReactionsRow.kt | 2 +- .../amethyst/ui/note/ZapCustomDialog.kt | 4 +-- .../ui/screen/loggedIn/AccountViewModel.kt | 12 +++++---- 7 files changed, 38 insertions(+), 20 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 f287b3011f..178e5f2648 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -137,7 +137,7 @@ class Account( } } - fun createZapRequestFor(note: Note, pollOption: Int?, message: String = ""): LnZapRequestEvent? { + fun createZapRequestFor(note: Note, pollOption: Int?, message: String = "", zapType: LnZapEvent.ZapType): LnZapRequestEvent? { if (!isWriteable()) return null note.event?.let { event -> @@ -147,7 +147,8 @@ class Account( ?: localRelays.map { it.url }.toSet(), loggedIn.privKey!!, pollOption, - message + message, + zapType ) } return null @@ -171,14 +172,15 @@ class Account( return createZapRequestFor(user) } - fun createZapRequestFor(userPubKeyHex: String, message: String = ""): LnZapRequestEvent? { + fun createZapRequestFor(userPubKeyHex: String, message: String = "", zapType: LnZapEvent.ZapType): LnZapRequestEvent? { if (!isWriteable()) return null return LnZapRequestEvent.create( userPubKeyHex, userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } ?: localRelays.map { it.url }.toSet(), loggedIn.privKey!!, - message + message, + zapType ) } 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 5452aa36ab..34fddc02ac 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 @@ -25,10 +25,12 @@ class LnZapRequestEvent( privateKey: ByteArray, pollOption: Int?, message: String, + zapType: LnZapEvent.ZapType, createdAt: Long = Date().time / 1000 ): LnZapRequestEvent { val content = message - val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() + var privkey = privateKey + var pubKey = Utils.pubkeyCreate(privateKey).toHexKey() var tags = listOf( listOf("e", originalNote.id()), listOf("p", originalNote.pubKey()), @@ -40,9 +42,13 @@ class LnZapRequestEvent( if (pollOption != null && pollOption >= 0) { tags = tags + listOf(listOf(POLL_OPTION, pollOption.toString())) } - + if (zapType == LnZapEvent.ZapType.ANONYMOUS) { + tags = tags + listOf(listOf("anon", "")) + privkey = Utils.privkeyCreate() + pubKey = Utils.pubkeyCreate(privkey).toHexKey() + } val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = Utils.sign(id, privateKey) + val sig = Utils.sign(id, privkey) return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } @@ -51,17 +57,24 @@ class LnZapRequestEvent( relays: Set, privateKey: ByteArray, message: String, + zapType: LnZapEvent.ZapType, createdAt: Long = Date().time / 1000 ): LnZapRequestEvent { val content = message - val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() - val tags = listOf( + var privkey = privateKey + var pubKey = Utils.pubkeyCreate(privateKey).toHexKey() + var tags = listOf( listOf("p", userHex), listOf("relays") + relays ) + if (zapType == LnZapEvent.ZapType.ANONYMOUS) { + tags = tags + listOf(listOf("anon", "")) + privkey = Utils.privkeyCreate() + pubKey = Utils.pubkeyCreate(privkey).toHexKey() + } val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = Utils.sign(id, privateKey) + val sig = Utils.sign(id, privkey) return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt index bca274b3c6..06e38c773c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt @@ -37,6 +37,7 @@ import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver +import com.vitorpamplona.amethyst.service.model.LnZapEvent import kotlinx.coroutines.launch @Composable @@ -135,7 +136,7 @@ fun InvoiceRequest( Button( modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp), onClick = { - val zapRequest = account.createZapRequestFor(toUserPubKeyHex, message) + val zapRequest = account.createZapRequestFor(toUserPubKeyHex, message, LnZapEvent.ZapType.PUBLIC) LightningAddressResolver().lnAddressInvoice( lud16, 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 6a56c847fe..da0f35936a 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 @@ -253,7 +253,7 @@ fun ZapVote( zappingProgress = it } }, - type = LnZapEvent.ZapType.PUBLIC + zapType = LnZapEvent.ZapType.PUBLIC ) } } else { 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 77e7ddfeee..5a0d7a3b47 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 @@ -365,7 +365,7 @@ fun ZapReaction( zappingProgress = it } }, - type = LnZapEvent.ZapType.PUBLIC + zapType = LnZapEvent.ZapType.PUBLIC ) } } else if (account.zapAmountChoices.size > 1) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt index 2fcde04b35..5fe601a6ab 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt @@ -59,7 +59,6 @@ fun ZapCustomDialog(onClose: () -> Unit, account: Account, accountViewModel: Acc val context = LocalContext.current val scope = rememberCoroutineScope() val postViewModel: ZapOptionstViewModel = viewModel() - postViewModel.load(account) LaunchedEffect(account) { postViewModel.load(account) } @@ -68,6 +67,7 @@ fun ZapCustomDialog(onClose: () -> Unit, account: Account, accountViewModel: Acc val zapTypes = listOf( Pair(LnZapEvent.ZapType.PUBLIC, "Public"), + Pair(LnZapEvent.ZapType.ANONYMOUS, "Anonymous"), Pair(LnZapEvent.ZapType.NONZAP, "Non-Zap") ) @@ -115,7 +115,7 @@ fun ZapCustomDialog(onClose: () -> Unit, account: Account, accountViewModel: Acc zappingProgress = it } }, - type = selectedZapType.first + zapType = selectedZapType.first ) } onClose() 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 513c5b1505..c88352b6c7 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 @@ -53,7 +53,7 @@ class AccountViewModel(private val account: Account) : ViewModel() { account.delete(account.boostsTo(note)) } - fun zap(note: Note, amount: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit, type: LnZapEvent.ZapType) { + fun zap(note: Note, amount: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit, zapType: LnZapEvent.ZapType) { val lud16 = note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim() if (lud16.isNullOrBlank()) { @@ -61,11 +61,13 @@ class AccountViewModel(private val account: Account) : ViewModel() { return } - var zapRequest = account.createZapRequestFor(note, pollOption, message) - var zapRequestJson = zapRequest?.toJson() + var zapRequestJson = "" - if (type == LnZapEvent.ZapType.NONZAP) { - zapRequestJson = "" + if (zapType != LnZapEvent.ZapType.NONZAP) { + val zapRequest = account.createZapRequestFor(note, pollOption, message, zapType) + if (zapRequest != null) { + zapRequestJson = zapRequest.toJson() + } } onProgress(0.10f) From 37dab1c8b614af951bf498d56ee5c51b049f9440 Mon Sep 17 00:00:00 2001 From: Believethehype Date: Mon, 10 Apr 2023 13:54:29 +0200 Subject: [PATCH 113/145] Update LnZapEvent.kt --- .../com/vitorpamplona/amethyst/service/model/LnZapEvent.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 ca8eedee6d..1812fb8030 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 @@ -72,8 +72,8 @@ class LnZapEvent( enum class ZapType() { PUBLIC, - PRIVATE, // not implemented - ANONYMOUS, // not implemented + PRIVATE, // not yet implemented + ANONYMOUS, NONZAP } } From 401d807d29712bf7a6ac120044f481397c597648 Mon Sep 17 00:00:00 2001 From: Zoltan <34719275+ZsZolee@users.noreply.github.com> Date: Tue, 11 Apr 2023 10:26:19 +0200 Subject: [PATCH 114/145] Added new Hungarian translations --- app/src/main/res/values-hu/strings.xml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 81296d2437..dc5073f2a0 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -263,9 +263,30 @@ nsec / hex privát kulcs Hozzájárulás összege sats-ban + Szavazás Létrehozása + Szükséges mezők: + Zap-et kapják + Szavazás elsődleges leírása… + Szavazás %s megoszlása + Szavazás opcióinak leírása + Kiegészítő mezők: + Zap minimum + Zap maximum + Konszenzus + (0–100)% + Szavazás lezárása + napok + A szavazásra nem lehet már új szavazatot leadni + Zap összege + Az ilyen típusú szavazásokon felhasználónként csak egy szavazat engedélyezett + "%1$s esemény keresése" Nyilvános üzenet hozzáadása Köszönöm a kemény munkát! + + Létrehoz és Hozzáad + A szavazás létrehozója sajátjára nem szavazhat. + #zappoll From 9d27b8d47e705936b868c065373daff8661f4adc Mon Sep 17 00:00:00 2001 From: nalbaltaji Date: Sat, 15 Apr 2023 18:59:44 +0200 Subject: [PATCH 115/145] Intercept back press when drawer is opened to close drawer --- .../amethyst/ui/screen/loggedIn/MainScreen.kt | 7 +++++++ 1 file changed, 7 insertions(+) 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 e0293b5d25..49486609d5 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 @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn +import androidx.activity.compose.BackHandler import androidx.compose.animation.Crossfade import androidx.compose.animation.core.tween import androidx.compose.foundation.background @@ -19,6 +20,7 @@ import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController @@ -34,10 +36,12 @@ import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.navigation.currentRoute import com.vitorpamplona.amethyst.ui.screen.AccountState import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterialApi::class) @Composable fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: AccountStateViewModel, startingPage: String? = null) { + val coroutineScope = rememberCoroutineScope() val navController = rememberNavController() val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed)) val sheetState = rememberModalBottomSheetState( @@ -64,6 +68,9 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun }, drawerContent = { DrawerContent(navController, scaffoldState, sheetState, accountViewModel) + BackHandler(enabled = scaffoldState.drawerState.isOpen) { + coroutineScope.launch { scaffoldState.drawerState.close() } + } }, floatingActionButton = { FloatingButtons(navController, accountStateViewModel) From 50b0b5af37f2f2e9a7d72d437b1808f1466c43de Mon Sep 17 00:00:00 2001 From: Ryo Kanbayashi Date: Sun, 16 Apr 2023 22:12:54 +0900 Subject: [PATCH 116/145] fix bug: quote repost is tied up to a post which is replyed a little while ago. --- .../com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt | 1 + .../com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt index 516ac6e52e..9f28695088 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt @@ -53,6 +53,7 @@ class NewMessageTagger(var channel: Channel?, var mentions: List?, var rep message = message.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) 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 1a98c4e970..e924b06275 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 @@ -79,6 +79,7 @@ open class NewPostViewModel : ViewModel() { quote?.let { message = TextFieldValue(message.text + "\n\n@${it.idNote()}") + this.replyTos = emptyList() } canAddInvoice = account.userProfile().info?.lnAddress() != null From 180083147dcd059209523cda114380a3dbef9465 Mon Sep 17 00:00:00 2001 From: Ryo Kanbayashi Date: Sun, 16 Apr 2023 22:26:55 +0900 Subject: [PATCH 117/145] removed needless added newline. --- .../com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt index 9f28695088..516ac6e52e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt @@ -53,7 +53,6 @@ class NewMessageTagger(var channel: Channel?, var mentions: List?, var rep message = message.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) From 78c997403659ca25a384126ed8b8c3bff8ae12fb Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 18 Apr 2023 08:41:19 -0400 Subject: [PATCH 118/145] Removes invalidateFilters Logging --- .../java/com/vitorpamplona/amethyst/service/NostrDataSource.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 755fbf6076..eb564b247a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt @@ -154,7 +154,7 @@ abstract class NostrDataSource(val debugName: String) { // Refreshes observers in batches. private val bundler = BundledUpdate(250, Dispatchers.IO) { - println("DataSource: ${this.javaClass.simpleName} InvalidateFilters") + // println("DataSource: ${this.javaClass.simpleName} InvalidateFilters") // adds the time to perform the refresh into this delay // holding off new updates in case of heavy refresh routines. From 38e18cf0a753bfa39000781db428bf4441f07fb2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 18 Apr 2023 08:41:59 -0400 Subject: [PATCH 119/145] Simplifies comparison for Channels via Hex code instead of objects --- .../java/com/vitorpamplona/amethyst/model/Note.kt | 13 +++++++------ .../amethyst/ui/dal/GlobalFeedFilter.kt | 6 +++--- 2 files changed, 10 insertions(+), 9 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 f12698a335..7bf4537f7e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -53,13 +53,14 @@ open class Note(val idHex: String) { open fun idNote() = id().toNote() open fun idDisplayNote() = idNote().toShortenHex() - fun channel(): Channel? { - val channelHex = - (event as? ChannelMessageEvent)?.channel() - ?: (event as? ChannelMetadataEvent)?.channel() - ?: (event as? ChannelCreateEvent)?.id + fun channelHex(): HexKey? { + return (event as? ChannelMessageEvent)?.channel() + ?: (event as? ChannelMetadataEvent)?.channel() + ?: (event as? ChannelCreateEvent)?.id + } - return channelHex?.let { LocalCache.checkGetOrCreateChannel(it) } + fun channel(): Channel? { + return channelHex()?.let { LocalCache.checkGetOrCreateChannel(it) } } open fun address(): ATag? = null 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 e902c2f98c..bd441642ae 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 @@ -9,7 +9,7 @@ object GlobalFeedFilter : FeedFilter() { lateinit var account: Account override fun feed(): List { - val followChannels = account.followingChannels() + val followChannels = account.followingChannels val followUsers = account.followingKeySet() val now = System.currentTimeMillis() / 1000 @@ -19,7 +19,7 @@ object GlobalFeedFilter : FeedFilter() { it.event is BaseTextNoteEvent && it.replyTo.isNullOrEmpty() } .filter { - val channel = it.channel() + val channel = it.channelHex() // does not show events already in the public chat list (channel == null || channel !in followChannels) && // does not show people the user already follows @@ -38,7 +38,7 @@ object GlobalFeedFilter : FeedFilter() { it.event is LongTextNoteEvent && it.replyTo.isNullOrEmpty() } .filter { - val channel = it.channel() + val channel = it.channelHex() // does not show events already in the public chat list (channel == null || channel !in followChannels) && // does not show people the user already follows From a2c86589724372d79444c406e525c0d1cebfd98a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 18 Apr 2023 08:42:32 -0400 Subject: [PATCH 120/145] Only calculates nip05 after identifying if it is correct. --- .../vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt index 53c5702141..cebfe1fdde 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt @@ -96,7 +96,6 @@ fun ObserveDisplayNip05Status(baseUser: User, columnModifier: Modifier = Modifie user.nip05()?.let { nip05 -> if (nip05.split("@").size == 2) { - val nip05Verified by nip05VerificationAsAState(user.info!!, user.pubkeyHex) Column(modifier = columnModifier) { Row(verticalAlignment = Alignment.CenterVertically) { if (nip05.split("@")[0] != "_") { @@ -108,6 +107,7 @@ fun ObserveDisplayNip05Status(baseUser: User, columnModifier: Modifier = Modifie ) } + val nip05Verified by nip05VerificationAsAState(user.info!!, user.pubkeyHex) if (nip05Verified == null) { Icon( tint = Color.Yellow, From 35647a836185c9241fd1376aaff657bc8b65a4f0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 18 Apr 2023 08:42:58 -0400 Subject: [PATCH 121/145] Fixes DM link in Message Notifications --- .../amethyst/ui/note/MessageSetCompose.kt | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt index bf0fa2cdb8..7629b5ca01 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt @@ -26,7 +26,9 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.vitorpamplona.amethyst.NotificationCache import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent +import com.vitorpamplona.amethyst.service.model.PrivateDmEvent import com.vitorpamplona.amethyst.ui.screen.MessageSetCard import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.coroutines.Dispatchers @@ -64,14 +66,29 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, isInnerNote: Boolean = fal Column( modifier = Modifier.background(backgroundColor).combinedClickable( onClick = { - if (noteEvent !is ChannelMessageEvent) { - navController.navigate("Note/${note.idHex}") { - launchSingleTop = true - } - } else { + if (noteEvent is ChannelMessageEvent) { note.channel()?.let { navController.navigate("Channel/${it.idHex}") } + } else if (noteEvent is PrivateDmEvent) { + val replyAuthorBase = + (note.event as? PrivateDmEvent) + ?.recipientPubKey() + ?.let { LocalCache.getOrCreateUser(it) } + + var userToComposeOn = note.author!! + + if (replyAuthorBase != null) { + if (note.author == accountViewModel.userProfile()) { + userToComposeOn = replyAuthorBase + } + } + + navController.navigate("Room/${userToComposeOn.pubkeyHex}") + } else { + navController.navigate("Note/${note.idHex}") { + launchSingleTop = true + } } }, onLongClick = { popupExpanded = true } From f64db1271be744473d9001cc671c447e83d6d7e0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 18 Apr 2023 08:44:11 -0400 Subject: [PATCH 122/145] Uses createdAt instead of the card object, which can change randomly, to determine when to call LaunchedEffects. --- .../com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt | 2 +- .../com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt index 7629b5ca01..8deb5bcb1d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt @@ -48,7 +48,7 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, isInnerNote: Boolean = fal } else { var isNew by remember { mutableStateOf(false) } - LaunchedEffect(key1 = messageSetCard) { + LaunchedEffect(key1 = messageSetCard.createdAt()) { withContext(Dispatchers.IO) { isNew = messageSetCard.createdAt() > NotificationCache.load(routeForLastRead) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index b001cce847..a8c7663569 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -61,7 +61,7 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun } else { var isNew by remember { mutableStateOf(false) } - LaunchedEffect(key1 = multiSetCard) { + LaunchedEffect(key1 = multiSetCard.createdAt()) { withContext(Dispatchers.IO) { isNew = multiSetCard.createdAt > NotificationCache.load(routeForLastRead) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt index f9512cde8a..7d00942951 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt @@ -43,7 +43,7 @@ fun ZapUserSetCompose(zapSetCard: ZapUserSetCard, isInnerNote: Boolean = false, var isNew by remember { mutableStateOf(false) } - LaunchedEffect(key1 = zapSetCard) { + LaunchedEffect(key1 = zapSetCard.createdAt()) { withContext(Dispatchers.IO) { isNew = zapSetCard.createdAt > NotificationCache.load(routeForLastRead) From e4fc00669e26e824ec0f2b47128dd84eed772716 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 18 Apr 2023 08:44:32 -0400 Subject: [PATCH 123/145] Compares users by reference. --- .../java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index a8c7663569..5376499053 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -244,7 +244,7 @@ fun FastNoteAuthorPicture( val userState by author.live().metadata.observeAsState() val user = userState?.user ?: return - val showFollowingMark = userAccount.isFollowingCached(user) || user == userAccount + val showFollowingMark = userAccount.isFollowingCached(user) || user === userAccount UserPicture( userHex = user.pubkeyHex, From 47e2683aa359de9f1572d25dcaaf9801dcb31b19 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 18 Apr 2023 08:45:02 -0400 Subject: [PATCH 124/145] Moves firstIsTaggedHash search to the IO thread. --- .../java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 be5075c253..807a408981 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 @@ -582,7 +582,9 @@ fun DisplayFollowingHashtagsInPost( var firstTag by remember { mutableStateOf(null) } LaunchedEffect(key1 = noteEvent) { - firstTag = noteEvent.firstIsTaggedHashes(account.followingTagSet()) + withContext(Dispatchers.IO) { + firstTag = noteEvent.firstIsTaggedHashes(account.followingTagSet()) + } } Column() { From 11bf702e022624431635d10214fe0d771564788c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 18 Apr 2023 08:45:34 -0400 Subject: [PATCH 125/145] Moves DropDown state to the IO thread. --- .../vitorpamplona/amethyst/ui/note/NoteCompose.kt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 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 807a408981..3d03c513cd 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 @@ -1110,12 +1110,14 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, } LaunchedEffect(key1 = note) { - state = DropDownParams( - accountViewModel.isFollowing(note.author), - accountViewModel.isInPrivateBookmarks(note), - accountViewModel.isInPublicBookmarks(note), - accountViewModel.isLoggedUser(note.author) - ) + withContext(Dispatchers.IO) { + state = DropDownParams( + accountViewModel.isFollowing(note.author), + accountViewModel.isInPrivateBookmarks(note), + accountViewModel.isInPublicBookmarks(note), + accountViewModel.isLoggedUser(note.author) + ) + } } DropdownMenu( From df45d68b74d13b057e18fc45e76852c18781e05c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 18 Apr 2023 08:45:50 -0400 Subject: [PATCH 126/145] Caches user profile in Account. --- .../main/java/com/vitorpamplona/amethyst/model/Account.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 178e5f2648..5a36d72975 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -54,8 +54,14 @@ class Account( val liveLanguages: AccountLiveData = AccountLiveData(this) val saveable: AccountLiveData = AccountLiveData(this) + var userProfileCache: User? = null + fun userProfile(): User { - return LocalCache.getOrCreateUser(loggedIn.pubKey.toHexKey()) + return userProfileCache ?: run { + val myUser: User = LocalCache.getOrCreateUser(loggedIn.pubKey.toHexKey()) + userProfileCache = myUser + myUser + } } fun followingChannels(): List { From 12b0afd9c62202178cad8812ac6c8c33e17e986d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 18 Apr 2023 08:46:12 -0400 Subject: [PATCH 127/145] Display note IDHex when measuring time. --- .../main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3d03c513cd..8400d174b3 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 @@ -96,7 +96,7 @@ fun NoteCompose( ) } - Log.d("Time", "Note Compose in $elapsed for ${baseNote.event?.kind()} ${baseNote.event?.content()?.split("\n")?.get(0)?.take(100)}") + Log.d("Time", "Note Compose in $elapsed for ${baseNote.idHex} ${baseNote.event?.kind()} ${baseNote.event?.content()?.split("\n")?.get(0)?.take(100)}") } @OptIn(ExperimentalFoundationApi::class) From c0946f5c0ee8147244419aa5908aedc3bccaee2d Mon Sep 17 00:00:00 2001 From: Ryo Kanbayashi Date: Wed, 19 Apr 2023 22:50:32 +0900 Subject: [PATCH 128/145] fix bug: quote repost is tied up to a post which is replyed a little while ago. (2) --- .../com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 e924b06275..8be28a4913 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 @@ -72,14 +72,13 @@ open class NewPostViewModel : ViewModel() { this.mentions = currentMentions.plus(replyUser) } } - } ?: { + } ?: run { replyTos = null mentions = null } quote?.let { message = TextFieldValue(message.text + "\n\n@${it.idNote()}") - this.replyTos = emptyList() } canAddInvoice = account.userProfile().info?.lnAddress() != null From 5afa333fab37face44b70beae500152919b8bf65 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 19 Apr 2023 09:58:11 -0400 Subject: [PATCH 129/145] Migration to Android Studio 2022.2.1 and Gradle 8 --- build.gradle | 4 ++-- gradle.properties | 4 +++- gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index ee67b5b0cc..b44afca7a8 100644 --- a/build.gradle +++ b/build.gradle @@ -10,8 +10,8 @@ buildscript { } }// Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '7.4.2' apply false - id 'com.android.library' version '7.4.2' apply false + id 'com.android.application' version '8.0.0' apply false + id 'com.android.library' version '8.0.0' apply false id 'org.jetbrains.kotlin.android' version '1.8.10' apply false id 'org.jetbrains.kotlin.jvm' version '1.8.10' apply false } diff --git a/gradle.properties b/gradle.properties index c9ecf2f958..e4e68fdb17 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,4 +21,6 @@ kotlin.code.style=official # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true -android.enableR8.fullMode=true \ No newline at end of file +android.enableR8.fullMode=true +android.defaults.buildfeatures.buildconfig=true +android.nonFinalResIds=false \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c1070955d4..289cef2848 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Wed Jan 04 09:23:50 EST 2023 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME From 4e09d9e54ac5030d08e3cd9df0b5f8aa36729715 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 19 Apr 2023 10:33:02 -0400 Subject: [PATCH 130/145] Performance Improvements for Zaps in Polls. --- .../amethyst/model/LocalCache.kt | 2 +- .../amethyst/service/NostrDataSource.kt | 2 +- .../amethyst/service/model/LnZapEvent.kt | 68 +++++++++---------- .../service/model/LnZapEventInterface.kt | 2 - .../service/model/LnZapRequestEvent.kt | 6 +- .../amethyst/service/model/PollNoteEvent.kt | 19 ++---- .../amethyst/ui/note/PollNote.kt | 11 ++- .../amethyst/ui/note/PollNoteViewModel.kt | 65 +++++++++--------- 8 files changed, 79 insertions(+), 96 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 cef63d463f..1705175d57 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -595,7 +595,7 @@ object LocalCache { // Already processed this event. if (note.event != null) return - val zapRequest = event.containedPost()?.id?.let { getOrCreateNote(it) } + val zapRequest = event.zapRequest?.id?.let { getOrCreateNote(it) } val author = getOrCreateUser(event.pubKey) val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) } 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 eb564b247a..6d4fbadf99 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt @@ -76,7 +76,7 @@ abstract class NostrDataSource(val debugName: String) { is DeletionEvent -> LocalCache.consume(event) is LnZapEvent -> { - event.containedPost()?.let { onEvent(it, subscriptionId, relay) } + event.zapRequest?.let { onEvent(it, subscriptionId, relay) } LocalCache.consume(event) } is LnZapRequestEvent -> LocalCache.consume(event) 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 1812fb8030..5f08c6a6e3 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 @@ -4,7 +4,6 @@ import android.util.Log import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil import com.vitorpamplona.amethyst.service.relays.Client -import java.math.BigDecimal class LnZapEvent( id: HexKey, @@ -14,25 +13,37 @@ class LnZapEvent( content: String, sig: HexKey ) : LnZapEventInterface, Event(id, pubKey, createdAt, kind, tags, content, sig) { + // This event is also kept in LocalCache (same object) + @Transient val zapRequest: LnZapRequestEvent? - override fun zappedPost() = tags - .filter { it.firstOrNull() == "e" } - .mapNotNull { it.getOrNull(1) } - - override fun zappedPollOption(): Int? = containedPost()?.tags - ?.filter { it.firstOrNull() == POLL_OPTION } - ?.getOrNull(0)?.getOrNull(1)?.toInt() - - override fun zappedAuthor() = tags - .filter { it.firstOrNull() == "p" } - .mapNotNull { it.getOrNull(1) } - - override fun zappedRequestAuthor(): String? = containedPost()?.pubKey() - - override fun amount(): BigDecimal? { - return amount + private fun containedPost(): LnZapRequestEvent? = try { + description()?.ifBlank { null }?.let { + fromJson(it, Client.lenient) + } as? LnZapRequestEvent + } catch (e: Exception) { + Log.e("LnZapEvent", "Failed to Parse Contained Post ${description()}", e) + null } + init { + zapRequest = containedPost() + } + + override fun zappedPost() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] } + + override fun zappedAuthor() = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] } + + override fun zappedPollOption(): Int? = try { + zapRequest?.tags?.firstOrNull { it.size > 1 && it[0] == POLL_OPTION }?.get(1)?.toInt() + } catch (e: Exception) { + Log.e("LnZapEvent", "ZappedPollOption failed to parse", e) + null + } + + override fun zappedRequestAuthor(): String? = zapRequest?.pubKey() + + override fun amount() = amount + // Keeps this as a field because it's a heavier function used everywhere. val amount by lazy { try { @@ -42,29 +53,14 @@ class LnZapEvent( null } } + override fun message(): String { - return message - } - val message = content - - override fun containedPost(): Event? = try { - description()?.ifBlank { null }?.let { - fromJson(it, Client.lenient) - } - } catch (e: Exception) { - Log.e("LnZapEvent", "Failed to Parse Contained Post ${description()}", e) - null + return content } - private fun lnInvoice(): String? = tags - .filter { it.firstOrNull() == "bolt11" } - .mapNotNull { it.getOrNull(1) } - .firstOrNull() + private fun lnInvoice() = tags.firstOrNull { it.size > 1 && it[0] == "bolt11" }?.get(1) - private fun description(): String? = tags - .filter { it.firstOrNull() == "description" } - .mapNotNull { it.getOrNull(1) } - .firstOrNull() + private fun description() = tags.firstOrNull { it.size > 1 && it[0] == "description" }?.get(1) companion object { const val kind = 9735 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 b69e027817..ada0d609d3 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 @@ -16,7 +16,5 @@ interface LnZapEventInterface : EventInterface { fun amount(): BigDecimal? - fun containedPost(): Event? - fun message(): String } 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 34fddc02ac..eba7a18a9f 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 @@ -13,8 +13,10 @@ class LnZapRequestEvent( content: String, sig: HexKey ) : Event(id, pubKey, createdAt, kind, tags, content, sig) { - fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } - fun zappedAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } + + fun zappedPost() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] } + + fun zappedAuthor() = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] } companion object { const val kind = 9734 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 a6bcc89b8b..c91cd4c7a3 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 @@ -20,24 +20,17 @@ class PollNoteEvent( content: String, sig: HexKey ) : BaseTextNoteEvent(id, pubKey, createdAt, kind, tags, content, sig) { - fun pollOptions(): Map { - val map = mutableMapOf() - tags.filter { it.first() == POLL_OPTION } - .forEach { map[it[1].toInt()] = it[2] } - return map - } + fun pollOptions() = + tags.filter { it.size > 2 && it[0] == POLL_OPTION } + .associate { it[1].toInt() to it[2] } fun getTagInt(property: String): Int? { - val tagList = tags.filter { - it.firstOrNull() == property - } - val tag = tagList.getOrNull(0) - val s = tag?.getOrNull(1) + val number = tags.firstOrNull() { it.size > 1 && it[0] == property }?.get(1) - return if (s.isNullOrBlank() || s == "null") { + return if (number.isNullOrBlank() || number == "null") { null } else { - s.toInt() + number.toInt() } } 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 da0f35936a..26e83cf916 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 @@ -48,9 +48,6 @@ fun PollNote( accountViewModel: AccountViewModel, navController: NavController ) { - val accountState by accountViewModel.accountLiveData.observeAsState() - val account = accountState?.account ?: return - val zapsState by baseNote.live().zaps.observeAsState() val zappedNote = zapsState?.note ?: return @@ -72,7 +69,7 @@ fun PollNote( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 3.dp) ) { - if (zappedNote.author == account.userProfile() || zappedNote.isZappedBy(account.userProfile())) { + if (accountViewModel.isLoggedUser(zappedNote.author) || zappedNote.isZappedBy(accountViewModel.userProfile())) { ZapVote( baseNote, accountViewModel, @@ -90,7 +87,7 @@ fun PollNote( LinearProgressIndicator( modifier = Modifier.matchParentSize(), color = color, - progress = optionTally + progress = optionTally.toFloat() ) Row( @@ -101,7 +98,7 @@ fun PollNote( modifier = Modifier.padding(horizontal = 10.dp).width(40.dp) ) { Text( - text = "${(optionTally * 100).roundToInt()}%", + text = "${(optionTally.toFloat() * 100).roundToInt()}%", fontWeight = FontWeight.Bold ) } @@ -210,7 +207,7 @@ fun ZapVote( ) .show() } - } else if (zappedNote?.author == account.userProfile()) { + } else if (accountViewModel.isLoggedUser(zappedNote?.author)) { scope.launch { Toast .makeText( 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 e5d3d4cd84..4ce21c3723 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 @@ -5,6 +5,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.model.* import java.math.BigDecimal +import java.math.RoundingMode import java.util.* class PollNoteViewModel { @@ -16,7 +17,9 @@ class PollNoteViewModel { var valueMaximum: Int? = null var valueMinimum: Int? = null private var closedAt: Int? = null - var consensusThreshold: Float? = null + var consensusThreshold: BigDecimal? = null + + var totalZapped: BigDecimal = BigDecimal.ZERO fun load(note: Note?) { pollNote = note @@ -24,8 +27,10 @@ class PollNoteViewModel { pollOptions = pollEvent?.pollOptions() valueMaximum = pollEvent?.getTagInt(VALUE_MAXIMUM) valueMinimum = pollEvent?.getTagInt(VALUE_MINIMUM) - consensusThreshold = pollEvent?.getTagInt(CONSENSUS_THRESHOLD)?.toFloat()?.div(100) + consensusThreshold = pollEvent?.getTagInt(CONSENSUS_THRESHOLD)?.toFloat()?.div(100)?.toBigDecimal() closedAt = pollEvent?.getTagInt(CLOSED_AT) + + totalZapped = totalZapped() } fun isVoteAmountAtomic() = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum @@ -73,47 +78,39 @@ class PollNoteViewModel { return false } - fun optionVoteTally(op: Int): Float { - val tally = zappedPollOptionAmount(op).toFloat().div(zappedVoteTotal()) - return if (tally.isNaN()) { // catch div by 0 - 0f - } else { tally } - } - - private fun zappedVoteTotal(): Float { - var total = 0f - pollOptions?.keys?.forEach { - total += zappedPollOptionAmount(it).toFloat() + fun optionVoteTally(op: Int): BigDecimal { + return if (totalZapped.compareTo(BigDecimal.ZERO) > 0) { + zappedPollOptionAmount(op).divide(totalZapped, 2, RoundingMode.HALF_UP) + } else { + BigDecimal.ZERO } - 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 && it.zappedRequestAuthor() == user.pubkeyHex) { - return true - } + if (pollNote?.zaps?.any { it.key.author === user } == true) { + pollNote!!.zaps + .any { + val event = it.value?.event as? LnZapEvent + event?.zappedPollOption() == option && event.zappedRequestAuthor() == user.pubkeyHex } } return false } fun zappedPollOptionAmount(option: Int): BigDecimal { - return if (pollNote != null) { - pollNote!!.zaps.mapNotNull { it.value?.event } - .filterIsInstance() - .mapNotNull { - val zappedOption = it.zappedPollOption() - if (zappedOption == option) { - it.amount - } else { null } - }.sumOf { it } - } else { - BigDecimal(0) - } + return pollNote?.zaps?.values?.sumOf { + val event = it?.event as? LnZapEvent + if (event?.zappedPollOption() == option) { + event.amount ?: BigDecimal(0) + } else { + BigDecimal(0) + } + } ?: BigDecimal(0) + } + + fun totalZapped(): BigDecimal { + return pollNote?.zaps?.values?.sumOf { + (it?.event as? LnZapEvent)?.amount ?: BigDecimal(0) + } ?: BigDecimal(0) } } From b55f999efb5ef67353be4a1e589be9e0b146bbcc Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 19 Apr 2023 10:33:52 -0400 Subject: [PATCH 131/145] Slight performance improvements on tag search. --- .../vitorpamplona/amethyst/service/model/PrivateDmEvent.kt | 4 ++-- .../com/vitorpamplona/amethyst/service/model/ReactionEvent.kt | 4 ++-- .../com/vitorpamplona/amethyst/service/model/ReportEvent.kt | 4 ++-- .../com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PrivateDmEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PrivateDmEvent.kt index 89e1d98e67..c820d0ca2f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PrivateDmEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PrivateDmEvent.kt @@ -21,7 +21,7 @@ class PrivateDmEvent( * nip-04 EncryptedDmEvent but may omit the recipient, too. This value can be queried and used * for initial messages. */ - fun recipientPubKey() = tags.firstOrNull { it.firstOrNull() == "p" }?.run { Hex.decode(this[1]).toHexKey() } // makes sure its a valid one + fun recipientPubKey() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.run { Hex.decode(this[1]).toHexKey() } // makes sure its a valid one /** * To be fully compatible with nip-04, we read e-tags that are in violation to nip-18. @@ -29,7 +29,7 @@ class PrivateDmEvent( * Nip-18 messages should refer to other events by inline references in the content like * `[](e/c06f795e1234a9a1aecc731d768d4f3ca73e80031734767067c82d67ce82e506). */ - fun replyTo() = tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1) + fun replyTo() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1) fun plainContent(privKey: ByteArray, pubKey: ByteArray): String? { return try { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt index f89bb29775..13188520c4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt @@ -14,8 +14,8 @@ class ReactionEvent( sig: HexKey ) : Event(id, pubKey, createdAt, kind, tags, content, sig) { - fun originalPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } - fun originalAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } + fun originalPost() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] } + fun originalAuthor() = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] } companion object { const val kind = 7 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt index 42f33c3c52..d3bd9dd97a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt @@ -30,7 +30,7 @@ class ReportEvent( } fun reportedPost() = tags - .filter { it.firstOrNull() == "e" && it.getOrNull(1) != null } + .filter { it.size > 1 && it[0] == "e" } .map { ReportedKey( it[1], @@ -39,7 +39,7 @@ class ReportEvent( } fun reportedAuthor() = tags - .filter { it.firstOrNull() == "p" && it.getOrNull(1) != null } + .filter { it.size > 1 && it[0] == "p" } .map { ReportedKey( it[1], 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 1a98c4e970..8be28a4913 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 @@ -72,7 +72,7 @@ open class NewPostViewModel : ViewModel() { this.mentions = currentMentions.plus(replyUser) } } - } ?: { + } ?: run { replyTos = null mentions = null } From 744a55f4972000a2a48d5cd04eda8bdb99f76d55 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 19 Apr 2023 10:34:10 -0400 Subject: [PATCH 132/145] Caching channel info JSON parse in NoteCompose. --- .../amethyst/ui/note/ChatroomMessageCompose.kt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index fee45ca0e9..bee9980155 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -246,30 +246,32 @@ fun ChatroomMessageCompose( Row(verticalAlignment = Alignment.CenterVertically) { val event = note.event if (event is ChannelCreateEvent) { + val channelInfo = event.channelInfo() Text( text = note.author?.toBestDisplayName() .toString() + " ${stringResource(R.string.created)} " + ( - event.channelInfo().name + channelInfo.name ?: "" ) + " ${stringResource(R.string.with_description_of)} '" + ( - event.channelInfo().about + channelInfo.about ?: "" ) + "', ${stringResource(R.string.and_picture)} '" + ( - event.channelInfo().picture + channelInfo.picture ?: "" ) + "'" ) } else if (event is ChannelMetadataEvent) { + val channelInfo = event.channelInfo() Text( text = note.author?.toBestDisplayName() .toString() + " ${stringResource(R.string.changed_chat_name_to)} '" + ( - event.channelInfo().name + channelInfo.name ?: "" ) + "$', {stringResource(R.string.description_to)} '" + ( - event.channelInfo().about + channelInfo.about ?: "" ) + "', ${stringResource(R.string.and_picture_to)} '" + ( - event.channelInfo().picture + channelInfo.picture ?: "" ) + "'" ) From b28f0bff742e180aef572a01dfea0aea034aebb4 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 19 Apr 2023 11:24:49 -0400 Subject: [PATCH 133/145] Updating build scripts to java 17 --- .github/workflows/build.yml | 4 ++-- .github/workflows/create-release.yml | 4 ++-- README.md | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6e6e2910b6..e7ef182e0d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,10 +14,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v1 with: - java-version: 11 + java-version: 17 - name: Cache gradle uses: actions/cache@v1 diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 853a6ad2b1..e788e06bfc 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -12,10 +12,10 @@ jobs: - name: Checkout code uses: actions/checkout@v2 - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v1 with: - java-version: 11 + java-version: 17 - name: Cache gradle uses: actions/cache@v1 diff --git a/README.md b/README.md index 27b39d5a28..7d2eb1ce50 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ Lastly, the user's account information (priv key/pub key) is stored in the Andro ## Setup Make sure to have the following pre-requisites installed: -1. Java 11 +1. Java 17 2. Android Studio 3. Android 8.0+ Phone or Emulation setup From dc8804933d28eb3fec7a777d716a0ccb8e8a1e08 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 19 Apr 2023 11:27:20 -0400 Subject: [PATCH 134/145] moving source code compatibility to java 17 --- app/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index fd5da4e0d3..2e96328bcc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -57,12 +57,12 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '11' + jvmTarget = '17' } buildFeatures { From 0b30c83143bbaa0ea4912c76392c6e32c4b96f82 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 19 Apr 2023 11:32:52 -0400 Subject: [PATCH 135/145] Moves coffee.xml to the right directory. --- app/src/main/res/{drawable-xxxhdpi => drawable}/coffee.xml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/res/{drawable-xxxhdpi => drawable}/coffee.xml (100%) diff --git a/app/src/main/res/drawable-xxxhdpi/coffee.xml b/app/src/main/res/drawable/coffee.xml similarity index 100% rename from app/src/main/res/drawable-xxxhdpi/coffee.xml rename to app/src/main/res/drawable/coffee.xml From dc745c3cdbab662df70b098023b3508260865f1e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 19 Apr 2023 11:33:03 -0400 Subject: [PATCH 136/145] disables errors due to missing translations --- app/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index 2e96328bcc..c31919d7da 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -78,6 +78,10 @@ android { excludes += '/META-INF/{AL2.0,LGPL2.1}' } } + + lintOptions { + disable 'MissingTranslation' + } } dependencies { From a816fa561a7634f0003b282eea39399a25dc9d43 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 19 Apr 2023 11:34:38 -0400 Subject: [PATCH 137/145] Declares the use of the camera as a feature. --- app/src/main/AndroidManifest.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 518e6a5397..3735c465f9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + From 6a9a321e2bfdcb7445f4acf96050a7d5e2a39b27 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 19 Apr 2023 13:00:52 -0400 Subject: [PATCH 138/145] Fixing format --- .../amethyst/ui/dal/NotificationFeedFilter.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 46e483c411..39334ca15c 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 @@ -23,14 +23,14 @@ object NotificationFeedFilter : AdditiveFeedFilter() { return collection.filter { it.event !is ChannelCreateEvent && - it.event !is ChannelMetadataEvent && - it.event !is LnZapRequestEvent && - it.event !is BadgeDefinitionEvent && - it.event !is BadgeProfilesEvent && - it.author !== loggedInUser && - it.event?.isTaggedUser(loggedInUserHex) ?: false && - (it.author == null || !account.isHidden(it.author!!.pubkeyHex)) && - tagsAnEventByUser(it, loggedInUser) + it.event !is ChannelMetadataEvent && + it.event !is LnZapRequestEvent && + it.event !is BadgeDefinitionEvent && + it.event !is BadgeProfilesEvent && + it.author !== loggedInUser && + it.event?.isTaggedUser(loggedInUserHex) ?: false && + (it.author == null || !account.isHidden(it.author!!.pubkeyHex)) && + tagsAnEventByUser(it, loggedInUser) } } From e775ae9ada6f191b8e4daf365f3ad4c2a21d8697 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 19 Apr 2023 13:54:00 -0400 Subject: [PATCH 139/145] Adding the new parameter. --- .../main/java/com/vitorpamplona/amethyst/model/LocalCache.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3dc33582b0..1beb05b369 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -255,7 +255,7 @@ object LocalCache { it.addReply(note) } - refreshObservers() + refreshObservers(note) } fun consume(event: BadgeDefinitionEvent) { From 47398e6e867c74e2e86611f060148e8361fdf220 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 19 Apr 2023 15:56:58 -0400 Subject: [PATCH 140/145] Starts with an empty set as database --- .../main/java/com/vitorpamplona/amethyst/model/LocalCache.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1beb05b369..ee46362889 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -762,7 +762,7 @@ object LocalCache { } } -class LocalCacheLiveData : LiveData>() { +class LocalCacheLiveData : LiveData>(setOf()) { // Refreshes observers in batches. private val bundler = BundledInsert(300, Dispatchers.Main) From 57b35398bebe2e8a6c960d441e7db50d3b5ca9bd Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 19 Apr 2023 15:57:17 -0400 Subject: [PATCH 141/145] Activates updates --- .../amethyst/ui/components/BundledUpdate.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/BundledUpdate.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/BundledUpdate.kt index 3d118e08cf..ea6dff88f1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/BundledUpdate.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/BundledUpdate.kt @@ -57,9 +57,9 @@ class BundledInsert( private var atomicSet = AtomicReference>(setOf()) fun invalidateList(newObject: T, onUpdate: (Set) -> Unit) { - // atomicSet.updateAndGet() { - // it + newObject - // } + atomicSet.updateAndGet() { + it + newObject + } if (onlyOneInBlock.getAndSet(true)) { return @@ -68,10 +68,9 @@ class BundledInsert( val scope = CoroutineScope(Job() + dispatcher) scope.launch { try { - // onUpdate(atomicSet.getAndSet(emptySet())) - onUpdate(emptySet()) + onUpdate(atomicSet.getAndSet(emptySet())) delay(delay) - // onUpdate(atomicSet.getAndSet(emptySet())) + onUpdate(atomicSet.getAndSet(emptySet())) } finally { withContext(NonCancellable) { onlyOneInBlock.set(false) From abdad7fbea85b88c8ca6a543f988d86bf1f3b7d3 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 19 Apr 2023 15:57:27 -0400 Subject: [PATCH 142/145] solve recursive method call --- .../com/vitorpamplona/amethyst/ui/dal/GlobalFeedFilter.kt | 8 ++++---- .../vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt | 4 ++-- .../amethyst/ui/dal/HomeConversationsFeedFilter.kt | 6 +++--- .../amethyst/ui/dal/HomeNewThreadFeedFilter.kt | 8 ++++---- .../amethyst/ui/dal/NotificationFeedFilter.kt | 6 +++--- 5 files changed, 16 insertions(+), 16 deletions(-) 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 b55a9adf2b..ebd4d2cdfc 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 @@ -9,17 +9,17 @@ object GlobalFeedFilter : AdditiveFeedFilter() { lateinit var account: Account override fun feed(): List { - val notes = applyFilter(LocalCache.notes.values) - val longFormNotes = applyFilter(LocalCache.addressables.values) + val notes = innerApplyFilter(LocalCache.notes.values) + val longFormNotes = innerApplyFilter(LocalCache.addressables.values) return sort(notes + longFormNotes) } override fun applyFilter(collection: Set): List { - return applyFilter(collection) + return innerApplyFilter(collection) } - private fun applyFilter(collection: Collection): List { + private fun innerApplyFilter(collection: Collection): List { val followChannels = account.followingChannels val followUsers = account.followingKeySet() val now = System.currentTimeMillis() / 1000 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt index c33593ff40..32354c4080 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt @@ -18,14 +18,14 @@ object HashtagFeedFilter : AdditiveFeedFilter() { } override fun feed(): List { - return sort(applyFilter(LocalCache.notes.values)) + return sort(innerApplyFilter(LocalCache.notes.values)) } override fun applyFilter(collection: Set): List { return applyFilter(collection) } - private fun applyFilter(collection: Collection): List { + private fun innerApplyFilter(collection: Collection): List { val myTag = tag ?: return emptyList() return collection 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 5f2f3e6e51..f188856123 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 @@ -10,14 +10,14 @@ object HomeConversationsFeedFilter : AdditiveFeedFilter() { lateinit var account: Account override fun feed(): List { - return sort(applyFilter(LocalCache.notes.values)) + return sort(innerApplyFilter(LocalCache.notes.values)) } override fun applyFilter(collection: Set): List { - return applyFilter(collection) + return innerApplyFilter(collection) } - private fun applyFilter(collection: Collection): List { + private fun innerApplyFilter(collection: Collection): List { val user = account.userProfile() val followingKeySet = user.cachedFollowingKeySet() val followingTagSet = user.cachedFollowingTagSet() 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 443c115260..3ddf1471a1 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 @@ -12,17 +12,17 @@ object HomeNewThreadFeedFilter : AdditiveFeedFilter() { lateinit var account: Account override fun feed(): List { - val notes = applyFilter(LocalCache.notes.values) - val longFormNotes = applyFilter(LocalCache.addressables.values) + val notes = innerApplyFilter(LocalCache.notes.values) + val longFormNotes = innerApplyFilter(LocalCache.addressables.values) return sort(notes + longFormNotes) } override fun applyFilter(collection: Set): List { - return applyFilter(collection) + return innerApplyFilter(collection) } - private fun applyFilter(collection: Collection): List { + private fun innerApplyFilter(collection: Collection): List { val user = account.userProfile() val followingKeySet = user.cachedFollowingKeySet() val followingTagSet = user.cachedFollowingTagSet() 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 39334ca15c..8cb1f62809 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 @@ -10,14 +10,14 @@ object NotificationFeedFilter : AdditiveFeedFilter() { lateinit var account: Account override fun feed(): List { - return sort(applyFilter(LocalCache.notes.values)) + return sort(innerApplyFilter(LocalCache.notes.values)) } override fun applyFilter(collection: Set): List { - return applyFilter(collection) + return innerApplyFilter(collection) } - private fun applyFilter(collection: Collection): List { + private fun innerApplyFilter(collection: Collection): List { val loggedInUser = account.userProfile() val loggedInUserHex = loggedInUser.pubkeyHex From 6981fe8f8ac8ed8ec48fef059c34db0e80dada30 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 19 Apr 2023 16:35:26 -0400 Subject: [PATCH 143/145] Adapting interfaces for the additive filter. --- .../amethyst/ui/dal/ChannelFeedFilter.kt | 8 +-- .../amethyst/ui/dal/ChatroomFeedFilter.kt | 12 ++--- .../amethyst/ui/dal/FeedFilter.kt | 6 +-- .../amethyst/ui/dal/GlobalFeedFilter.kt | 8 +-- .../amethyst/ui/dal/HashtagFeedFilter.kt | 10 ++-- .../ui/dal/HomeConversationsFeedFilter.kt | 8 +-- .../ui/dal/HomeNewThreadFeedFilter.kt | 8 +-- .../amethyst/ui/dal/NotificationFeedFilter.kt | 8 +-- .../amethyst/ui/screen/CardFeedViewModel.kt | 54 ++++++++++++++++--- 9 files changed, 81 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChannelFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChannelFeedFilter.kt index 14e4b09554..aa2921dceb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChannelFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChannelFeedFilter.kt @@ -23,13 +23,13 @@ object ChannelFeedFilter : AdditiveFeedFilter() { .reversed() } - override fun applyFilter(collection: Set): List { + override fun applyFilter(collection: Set): Set { return collection - .filter { it.idHex in channel.notes.keys } - .filter { account.isAcceptable(it) } + .filter { it.idHex in channel.notes.keys && account.isAcceptable(it) } + .toSet() } - override fun sort(collection: List): List { + override fun sort(collection: Set): List { return collection.sortedBy { it.createdAt() }.reversed() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt index d885ce7e4a..f96c686817 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomFeedFilter.kt @@ -31,22 +31,22 @@ object ChatroomFeedFilter : AdditiveFeedFilter() { .reversed() } - override fun applyFilter(collection: Set): List { + override fun applyFilter(collection: Set): Set { val myAccount = account val myUser = withUser - if (myAccount == null || myUser == null) return emptyList() + if (myAccount == null || myUser == null) return emptySet() val messages = myAccount .userProfile() - .privateChatrooms[myUser] ?: return emptyList() + .privateChatrooms[myUser] ?: return emptySet() return collection - .filter { it in messages.roomMessages } - .filter { account?.isAcceptable(it) == true } + .filter { it in messages.roomMessages && account?.isAcceptable(it) == true } + .toSet() } - override fun sort(collection: List): List { + override fun sort(collection: Set): List { return collection.sortedBy { it.createdAt() }.reversed() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt index 9254d766b6..505a959ef9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt @@ -19,15 +19,15 @@ abstract class FeedFilter { } abstract class AdditiveFeedFilter : FeedFilter() { - abstract fun applyFilter(collection: Set): List - abstract fun sort(collection: List): List + abstract fun applyFilter(collection: Set): Set + abstract fun sort(collection: Set): List @OptIn(ExperimentalTime::class) fun updateListWith(oldList: List, newItems: Set): List { val (feed, elapsed) = measureTimedValue { val newItemsToBeAdded = applyFilter(newItems) if (newItemsToBeAdded.isNotEmpty()) { - val newList = oldList + newItemsToBeAdded + val newList = oldList.toSet() + newItemsToBeAdded sort(newList).take(1000) } else { oldList 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 ebd4d2cdfc..f17d5ade58 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 @@ -15,11 +15,11 @@ object GlobalFeedFilter : AdditiveFeedFilter() { return sort(notes + longFormNotes) } - override fun applyFilter(collection: Set): List { + override fun applyFilter(collection: Set): Set { return innerApplyFilter(collection) } - private fun innerApplyFilter(collection: Collection): List { + private fun innerApplyFilter(collection: Collection): Set { val followChannels = account.followingChannels val followUsers = account.followingKeySet() val now = System.currentTimeMillis() / 1000 @@ -41,10 +41,10 @@ object GlobalFeedFilter : AdditiveFeedFilter() { // Do not show notes with the creation time exceeding the current time, as they will always stay at the top of the global feed, which is cheating. it.createdAt()!! <= now } - .toList() + .toSet() } - override fun sort(collection: List): List { + override fun sort(collection: Set): List { return collection.sortedBy { it.createdAt() }.reversed() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt index 32354c4080..d4fd4765e3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HashtagFeedFilter.kt @@ -21,12 +21,12 @@ object HashtagFeedFilter : AdditiveFeedFilter() { return sort(innerApplyFilter(LocalCache.notes.values)) } - override fun applyFilter(collection: Set): List { + override fun applyFilter(collection: Set): Set { return applyFilter(collection) } - private fun innerApplyFilter(collection: Collection): List { - val myTag = tag ?: return emptyList() + private fun innerApplyFilter(collection: Collection): Set { + val myTag = tag ?: return emptySet() return collection .asSequence() @@ -40,10 +40,10 @@ object HashtagFeedFilter : AdditiveFeedFilter() { it.event?.isTaggedHash(myTag) == true } .filter { account.isAcceptable(it) } - .toList() + .toSet() } - override fun sort(collection: List): List { + override fun sort(collection: Set): List { return collection.sortedBy { it.createdAt() }.reversed() } } 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 f188856123..6026e2669b 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 @@ -13,11 +13,11 @@ object HomeConversationsFeedFilter : AdditiveFeedFilter() { return sort(innerApplyFilter(LocalCache.notes.values)) } - override fun applyFilter(collection: Set): List { + override fun applyFilter(collection: Set): Set { return innerApplyFilter(collection) } - private fun innerApplyFilter(collection: Collection): List { + private fun innerApplyFilter(collection: Collection): Set { val user = account.userProfile() val followingKeySet = user.cachedFollowingKeySet() val followingTagSet = user.cachedFollowingTagSet() @@ -31,10 +31,10 @@ object HomeConversationsFeedFilter : AdditiveFeedFilter() { it.author?.let { !account.isHidden(it) } ?: true && !it.isNewThread() } - .toList() + .toSet() } - override fun sort(collection: List): List { + override fun sort(collection: Set): List { return collection.sortedBy { it.createdAt() }.reversed() } } 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 3ddf1471a1..d31a45c682 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 @@ -18,11 +18,11 @@ object HomeNewThreadFeedFilter : AdditiveFeedFilter() { return sort(notes + longFormNotes) } - override fun applyFilter(collection: Set): List { + override fun applyFilter(collection: Set): Set { return innerApplyFilter(collection) } - private fun innerApplyFilter(collection: Collection): List { + private fun innerApplyFilter(collection: Collection): Set { val user = account.userProfile() val followingKeySet = user.cachedFollowingKeySet() val followingTagSet = user.cachedFollowingTagSet() @@ -36,10 +36,10 @@ object HomeNewThreadFeedFilter : AdditiveFeedFilter() { it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true && it.isNewThread() } - .toList() + .toSet() } - override fun sort(collection: List): List { + override fun sort(collection: Set): List { return collection.sortedBy { it.createdAt() }.reversed() } } 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 8cb1f62809..ba746ebbad 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 @@ -13,11 +13,11 @@ object NotificationFeedFilter : AdditiveFeedFilter() { return sort(innerApplyFilter(LocalCache.notes.values)) } - override fun applyFilter(collection: Set): List { + override fun applyFilter(collection: Set): Set { return innerApplyFilter(collection) } - private fun innerApplyFilter(collection: Collection): List { + private fun innerApplyFilter(collection: Collection): Set { val loggedInUser = account.userProfile() val loggedInUserHex = loggedInUser.pubkeyHex @@ -31,10 +31,10 @@ object NotificationFeedFilter : AdditiveFeedFilter() { it.event?.isTaggedUser(loggedInUserHex) ?: false && (it.author == null || !account.isHidden(it.author!!.pubkeyHex)) && tagsAnEventByUser(it, loggedInUser) - } + }.toSet() } - override fun sort(collection: List): List { + override fun sort(collection: Set): List { return collection.sortedBy { it.createdAt() }.reversed() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt index f2a2eb9cc0..f7e1a24c0f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt @@ -14,7 +14,9 @@ import com.vitorpamplona.amethyst.service.model.LnZapEvent import com.vitorpamplona.amethyst.service.model.PrivateDmEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.RepostEvent +import com.vitorpamplona.amethyst.ui.components.BundledInsert import com.vitorpamplona.amethyst.ui.components.BundledUpdate +import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.dal.NotificationFeedFilter import kotlinx.coroutines.CoroutineScope @@ -29,7 +31,7 @@ import kotlin.time.measureTimedValue class NotificationViewModel : CardFeedViewModel(NotificationFeedFilter) -open class CardFeedViewModel(val dataSource: FeedFilter) : ViewModel() { +open class CardFeedViewModel(val localFilter: FeedFilter) : ViewModel() { private val _feedContent = MutableStateFlow(CardFeedState.Loading) val feedContent = _feedContent.asStateFlow() @@ -45,9 +47,9 @@ open class CardFeedViewModel(val dataSource: FeedFilter) : ViewModel() { @Synchronized private fun refreshSuspended() { - val notes = dataSource.loadTop() + val notes = localFilter.loadTop() - val thisAccount = (dataSource as? NotificationFeedFilter)?.account + val thisAccount = (localFilter as? NotificationFeedFilter)?.account val lastNotesCopy = if (thisAccount == lastAccount) lastNotes else null val oldNotesState = _feedContent.value @@ -55,18 +57,18 @@ open class CardFeedViewModel(val dataSource: FeedFilter) : ViewModel() { val newCards = convertToCard(notes.minus(lastNotesCopy)) if (newCards.isNotEmpty()) { lastNotes = notes - lastAccount = (dataSource as? NotificationFeedFilter)?.account + lastAccount = (localFilter as? NotificationFeedFilter)?.account updateFeed((oldNotesState.feed.value + newCards).distinctBy { it.id() }.sortedBy { it.createdAt() }.reversed()) } } else { val cards = convertToCard(notes) lastNotes = notes - lastAccount = (dataSource as? NotificationFeedFilter)?.account + lastAccount = (localFilter as? NotificationFeedFilter)?.account updateFeed(cards) } } - private fun convertToCard(notes: List): List { + private fun convertToCard(notes: Collection): List { val reactionsPerEvent = mutableMapOf>() notes .filter { it.event is ReactionEvent } @@ -171,6 +173,28 @@ open class CardFeedViewModel(val dataSource: FeedFilter) : ViewModel() { } } + fun refreshFromOldState(newItems: Set) { + val oldNotesState = _feedContent.value + + val thisAccount = (localFilter as? NotificationFeedFilter)?.account + val lastNotesCopy = if (thisAccount == lastAccount) lastNotes else null + + if (lastNotesCopy != null && localFilter is AdditiveFeedFilter && oldNotesState is CardFeedState.Loaded) { + val filteredNewList = localFilter.applyFilter(newItems) + val actuallyNew = filteredNewList.minus(lastNotesCopy) + + val newCards = convertToCard(actuallyNew) + if (newCards.isNotEmpty()) { + lastNotes = lastNotesCopy + newItems + lastAccount = (localFilter as? NotificationFeedFilter)?.account + updateFeed((oldNotesState.feed.value + newCards).distinctBy { it.id() }.sortedBy { it.createdAt() }.reversed()) + } + } else { + // Refresh Everything + refreshSuspended() + } + } + @OptIn(ExperimentalTime::class) private val bundler = BundledUpdate(250, Dispatchers.IO) { // adds the time to perform the refresh into this delay @@ -180,13 +204,29 @@ open class CardFeedViewModel(val dataSource: FeedFilter) : ViewModel() { } Log.d("Time", "${this.javaClass.simpleName} Card update $elapsed") } + private val bundlerInsert = BundledInsert>(250, Dispatchers.IO) fun invalidateData() { bundler.invalidate() } + @OptIn(ExperimentalTime::class) + fun invalidateInsertData(newItems: Set) { + bundlerInsert.invalidateList(newItems) { + val (value, elapsed) = measureTimedValue { + refreshFromOldState(it.flatten().toSet()) + } + Log.d("Time", "${this.javaClass.simpleName} Card additive update $elapsed") + } + } + private val cacheListener: (Set) -> Unit = { newNotes -> - invalidateData() + if (localFilter is AdditiveFeedFilter && _feedContent.value is CardFeedState.Loaded) { + invalidateInsertData(newNotes) + } else { + // Refresh Everything + invalidateData() + } } init { From 2ae54b544f340252e82f2c8d48cd65f05e5cc3fa Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Thu, 20 Apr 2023 08:12:26 -0300 Subject: [PATCH 144/145] wip: add tor socks support --- .../amethyst/LocalPreferences.kt | 6 +++- .../vitorpamplona/amethyst/model/Account.kt | 10 +++--- .../amethyst/service/relays/Client.kt | 2 +- .../amethyst/service/relays/Constants.kt | 2 +- .../amethyst/service/relays/Relay.kt | 8 ++++- .../amethyst/ui/navigation/DrawerContent.kt | 32 +++++++++++++++++-- 6 files changed, 49 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index c2c21bb244..d45180dee7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -46,6 +46,7 @@ private object PrefKeys { const val LATEST_CONTACT_LIST = "latestContactList" const val HIDE_DELETE_REQUEST_DIALOG = "hide_delete_request_dialog" const val HIDE_BLOCK_ALERT_DIALOG = "hide_block_alert_dialog" + const val USE_PROXY = "use_proxy" val LAST_READ: (String) -> String = { route -> "last_read_route_$route" } } @@ -195,6 +196,7 @@ object LocalPreferences { putString(PrefKeys.LATEST_CONTACT_LIST, Event.gson.toJson(account.backupContactList)) putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog) putBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, account.hideBlockAlertDialog) + putBoolean(PrefKeys.USE_PROXY, account.useProxy) }.apply() } @@ -250,6 +252,7 @@ object LocalPreferences { val hideDeleteRequestDialog = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false) val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false) + val useProxy = getBoolean(PrefKeys.USE_PROXY, false) val a = Account( Persona(privKey = privKey?.toByteArray(), pubKey = pubKey.toByteArray()), @@ -263,7 +266,8 @@ object LocalPreferences { zapPaymentRequestServer, hideDeleteRequestDialog, hideBlockAlertDialog, - latestContactList + latestContactList, + useProxy ) return a 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 5a36d72975..c5d0aa081a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -45,7 +45,8 @@ class Account( var zapPaymentRequest: Nip47URI? = null, var hideDeleteRequestDialog: Boolean = false, var hideBlockAlertDialog: Boolean = false, - var backupContactList: ContactListEvent? = null + var backupContactList: ContactListEvent? = null, + var useProxy: Boolean = false ) { var transientHiddenUsers: Set = setOf() @@ -692,7 +693,7 @@ class Account( fun activeRelays(): Array? { var usersRelayList = userProfile().latestContactList?.relays()?.map { val localFeedTypes = localRelays.firstOrNull() { localRelay -> localRelay.url == it.key }?.feedTypes ?: FeedType.values().toSet() - Relay(it.key, it.value.read, it.value.write, localFeedTypes) + Relay(it.key, it.value.read, it.value.write, localFeedTypes, useProxy) } ?: return null // Ugly, but forces nostr.band as the only search-supporting relay today. @@ -702,7 +703,8 @@ class Account( Constants.forcedRelayForSearch.url, Constants.forcedRelayForSearch.read, Constants.forcedRelayForSearch.write, - Constants.forcedRelayForSearch.feedTypes + Constants.forcedRelayForSearch.feedTypes, + useProxy ) } @@ -711,7 +713,7 @@ class Account( fun convertLocalRelays(): Array { return localRelays.map { - Relay(it.url, it.read, it.write, it.feedTypes) + Relay(it.url, it.read, it.write, it.feedTypes, useProxy) }.toTypedArray() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt index 6f841deb65..602c4f3473 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt @@ -76,7 +76,7 @@ object Client : RelayPool.Listener { } } else { /** temporary connection */ - Relay(relay, false, true, emptySet()).requestAndWatch() { + Relay(relay, false, true, emptySet(), false).requestAndWatch() { it.send(signedEvent) it.disconnect() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt index a167a2d99f..5e947a967c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt @@ -10,7 +10,7 @@ object Constants { fun convertDefaultRelays(): Array { return defaultRelays.map { - Relay(it.url, it.read, it.write, it.feedTypes) + Relay(it.url, it.read, it.write, it.feedTypes, false) }.toTypedArray() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt index 87407844b2..def4096cdd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt @@ -10,6 +10,8 @@ import okhttp3.Request import okhttp3.Response import okhttp3.WebSocket import okhttp3.WebSocketListener +import java.net.InetSocketAddress +import java.net.Proxy import java.util.Date enum class FeedType { @@ -20,9 +22,13 @@ class Relay( var url: String, var read: Boolean = true, var write: Boolean = true, - var activeTypes: Set = FeedType.values().toSet() + var activeTypes: Set = FeedType.values().toSet(), + useProxy: Boolean ) { + private var proxy = if (useProxy) Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", 9050)) else null + private val httpClient = OkHttpClient.Builder() + .proxy(proxy) .followRedirects(true) .followSslRedirects(true) .build() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index defddc0c90..824eb87b1b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -46,6 +46,7 @@ import androidx.navigation.NavHostController import coil.compose.AsyncImage import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ServiceManager import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.components.ResizeImage @@ -100,7 +101,12 @@ fun DrawerContent( } @Composable -fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffoldState: ScaffoldState, navController: NavController) { +fun ProfileContent( + baseAccountUser: User, + modifier: Modifier = Modifier, + scaffoldState: ScaffoldState, + navController: NavController +) { val coroutineScope = rememberCoroutineScope() val accountUserState by baseAccountUser.live().metadata.observeAsState() @@ -197,11 +203,17 @@ fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffol }) ) { Row() { - Text("${accountUserFollows.cachedFollowCount() ?: "--"}", fontWeight = FontWeight.Bold) + Text( + "${accountUserFollows.cachedFollowCount() ?: "--"}", + fontWeight = FontWeight.Bold + ) Text(stringResource(R.string.following)) } Row(modifier = Modifier.padding(start = 10.dp)) { - Text("${accountUserFollows.cachedFollowerCount() ?: "--"}", fontWeight = FontWeight.Bold) + Text( + "${accountUserFollows.cachedFollowerCount() ?: "--"}", + fontWeight = FontWeight.Bold + ) Text(stringResource(R.string.followers)) } } @@ -221,6 +233,7 @@ fun ListContent( ) { val coroutineScope = rememberCoroutineScope() var backupDialogOpen by remember { mutableStateOf(false) } + var checked by remember { mutableStateOf(account.useProxy) } Column(modifier = modifier.fillMaxHeight()) { if (accountUser != null) { @@ -259,6 +272,19 @@ fun ListContent( onClick = { backupDialogOpen = true } ) + IconRow( + title = "Enable Tor", + icon = R.drawable.ic_topics, + tint = MaterialTheme.colors.onBackground, + onClick = { + checked = !checked + println("changed tor to $checked") + account.useProxy = checked + ServiceManager.pause() + ServiceManager.start() + } + ) + Spacer(modifier = Modifier.weight(1f)) IconRow( From cf8747b19fc11e524561f0e13bd654a7c9550c3d Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Thu, 20 Apr 2023 15:30:04 -0300 Subject: [PATCH 145/145] add a checkbox to use tor in the login screen, set the proxy in the account model construction --- .../vitorpamplona/amethyst/LocalPreferences.kt | 8 ++++++-- .../vitorpamplona/amethyst/model/Account.kt | 9 +++++---- .../amethyst/service/relays/Client.kt | 3 ++- .../amethyst/service/relays/Constants.kt | 3 ++- .../amethyst/service/relays/Relay.kt | 5 +---- .../amethyst/ui/navigation/DrawerContent.kt | 6 ++++-- .../ui/screen/AccountStateViewModel.kt | 18 +++++++++++------- .../ui/screen/loggedOff/LoginScreen.kt | 16 +++++++++++++--- 8 files changed, 44 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index d45180dee7..40b6d817ea 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -17,6 +17,8 @@ import nostr.postr.Persona import nostr.postr.toHex import nostr.postr.toNpub import java.io.File +import java.net.InetSocketAddress +import java.net.Proxy import java.util.Locale // Release mode (!BuildConfig.DEBUG) always uses encrypted preferences @@ -196,7 +198,8 @@ object LocalPreferences { putString(PrefKeys.LATEST_CONTACT_LIST, Event.gson.toJson(account.backupContactList)) putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog) putBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, account.hideBlockAlertDialog) - putBoolean(PrefKeys.USE_PROXY, account.useProxy) + println(account.proxy != null) + putBoolean(PrefKeys.USE_PROXY, account.proxy != null) }.apply() } @@ -253,6 +256,7 @@ object LocalPreferences { val hideDeleteRequestDialog = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false) val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false) val useProxy = getBoolean(PrefKeys.USE_PROXY, false) + var proxy = if (useProxy) Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", 9050)) else null val a = Account( Persona(privKey = privKey?.toByteArray(), pubKey = pubKey.toByteArray()), @@ -267,7 +271,7 @@ object LocalPreferences { hideDeleteRequestDialog, hideBlockAlertDialog, latestContactList, - useProxy + proxy ) return a 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 c5d0aa081a..fadefa7902 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import nostr.postr.Persona +import java.net.Proxy import java.util.Locale val DefaultChannels = setOf( @@ -46,7 +47,7 @@ class Account( var hideDeleteRequestDialog: Boolean = false, var hideBlockAlertDialog: Boolean = false, var backupContactList: ContactListEvent? = null, - var useProxy: Boolean = false + var proxy: Proxy? ) { var transientHiddenUsers: Set = setOf() @@ -693,7 +694,7 @@ class Account( fun activeRelays(): Array? { var usersRelayList = userProfile().latestContactList?.relays()?.map { val localFeedTypes = localRelays.firstOrNull() { localRelay -> localRelay.url == it.key }?.feedTypes ?: FeedType.values().toSet() - Relay(it.key, it.value.read, it.value.write, localFeedTypes, useProxy) + Relay(it.key, it.value.read, it.value.write, localFeedTypes, proxy) } ?: return null // Ugly, but forces nostr.band as the only search-supporting relay today. @@ -704,7 +705,7 @@ class Account( Constants.forcedRelayForSearch.read, Constants.forcedRelayForSearch.write, Constants.forcedRelayForSearch.feedTypes, - useProxy + proxy ) } @@ -713,7 +714,7 @@ class Account( fun convertLocalRelays(): Array { return localRelays.map { - Relay(it.url, it.read, it.write, it.feedTypes, useProxy) + Relay(it.url, it.read, it.write, it.feedTypes, proxy) }.toTypedArray() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt index 602c4f3473..b1ef135a9b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt @@ -76,7 +76,8 @@ object Client : RelayPool.Listener { } } else { /** temporary connection */ - Relay(relay, false, true, emptySet(), false).requestAndWatch() { + /** TODO: set the proxy for this temporary connection */ + Relay(relay, false, true, emptySet(), null).requestAndWatch() { it.send(signedEvent) it.disconnect() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt index 5e947a967c..9f5477687a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Constants.kt @@ -10,7 +10,8 @@ object Constants { fun convertDefaultRelays(): Array { return defaultRelays.map { - Relay(it.url, it.read, it.write, it.feedTypes, false) + /** TODO: set the proxy */ + Relay(it.url, it.read, it.write, it.feedTypes, null) }.toTypedArray() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt index def4096cdd..1084f21c95 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt @@ -10,7 +10,6 @@ import okhttp3.Request import okhttp3.Response import okhttp3.WebSocket import okhttp3.WebSocketListener -import java.net.InetSocketAddress import java.net.Proxy import java.util.Date @@ -23,10 +22,8 @@ class Relay( var read: Boolean = true, var write: Boolean = true, var activeTypes: Set = FeedType.values().toSet(), - useProxy: Boolean + proxy: Proxy? ) { - private var proxy = if (useProxy) Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", 9050)) else null - private val httpClient = OkHttpClient.Builder() .proxy(proxy) .followRedirects(true) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index 824eb87b1b..36e1ac835f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -54,6 +54,8 @@ import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountBackupDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.coroutines.launch +import java.net.InetSocketAddress +import java.net.Proxy @OptIn(ExperimentalMaterialApi::class) @Composable @@ -233,7 +235,7 @@ fun ListContent( ) { val coroutineScope = rememberCoroutineScope() var backupDialogOpen by remember { mutableStateOf(false) } - var checked by remember { mutableStateOf(account.useProxy) } + var checked by remember { mutableStateOf(account.proxy != null) } Column(modifier = modifier.fillMaxHeight()) { if (accountUser != null) { @@ -279,7 +281,7 @@ fun ListContent( onClick = { checked = !checked println("changed tor to $checked") - account.useProxy = checked + account.proxy = if (checked) Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", 9050)) else null ServiceManager.pause() ServiceManager.start() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index 0118af4591..d6872f808c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -18,6 +18,8 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import nostr.postr.Persona import nostr.postr.bechToBytes +import java.net.InetSocketAddress +import java.net.Proxy import java.util.regex.Pattern class AccountStateViewModel() : ViewModel() { @@ -39,21 +41,22 @@ class AccountStateViewModel() : ViewModel() { } } - fun startUI(key: String) { + fun startUI(key: String, useProxy: Boolean) { val pattern = Pattern.compile(".+@.+\\.[a-z]+") val parsed = Nip19.uriToRoute(key) val pubKeyParsed = parsed?.hex?.toByteArray() + var proxy = if (useProxy) Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", 9050)) else null val account = if (key.startsWith("nsec")) { - Account(Persona(privKey = key.bechToBytes())) + Account(Persona(privKey = key.bechToBytes()), proxy = proxy) } else if (pubKeyParsed != null) { - Account(Persona(pubKey = pubKeyParsed)) + Account(Persona(pubKey = pubKeyParsed), proxy = proxy) } else if (pattern.matcher(key).matches()) { // Evaluate NIP-5 - Account(Persona()) + Account(Persona(), proxy = proxy) } else { - Account(Persona(Hex.decode(key))) + Account(Persona(Hex.decode(key)), proxy = proxy) } LocalPreferences.updatePrefsForLogin(account) @@ -66,8 +69,9 @@ class AccountStateViewModel() : ViewModel() { tryLoginExistingAccount() } - fun newKey() { - val account = Account(Persona()) + fun newKey(useProxy: Boolean) { + var proxy = if (useProxy) Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", 9050)) else null + val account = Account(Persona(), proxy = proxy) // saves to local preferences LocalPreferences.updatePrefsForLogin(account) startUI(account) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt index ec45c3aff3..6533d1c906 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt @@ -55,6 +55,7 @@ fun LoginPage( var dialogOpen by remember { mutableStateOf(false) } + val useProxy = remember { mutableStateOf(false) } Column( modifier = Modifier @@ -158,7 +159,7 @@ fun LoginPage( keyboardActions = KeyboardActions( onGo = { try { - accountViewModel.startUI(key.value.text) + accountViewModel.startUI(key.value.text, useProxy.value) } catch (e: Exception) { errorMessage = context.getString(R.string.invalid_key) } @@ -221,6 +222,15 @@ fun LoginPage( } } + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox( + checked = useProxy.value, + onCheckedChange = { useProxy.value = it } + ) + + Text("Enable Tor") + } + Spacer(modifier = Modifier.height(20.dp)) Box(modifier = Modifier.padding(40.dp, 0.dp, 40.dp, 0.dp)) { @@ -237,7 +247,7 @@ fun LoginPage( if (acceptedTerms.value && key.value.text.isNotBlank()) { try { - accountViewModel.startUI(key.value.text) + accountViewModel.startUI(key.value.text, useProxy.value) } catch (e: Exception) { errorMessage = context.getString(R.string.invalid_key) } @@ -265,7 +275,7 @@ fun LoginPage( .fillMaxWidth(), onClick = { if (acceptedTerms.value) { - accountViewModel.newKey() + accountViewModel.newKey(useProxy.value) } else { termsAcceptanceIsRequired = context.getString(R.string.acceptance_of_terms_is_required)