From 26a1624399c184d1260ebd2a66239251795464a3 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Mon, 22 Jan 2024 09:02:44 -0300 Subject: [PATCH 01/32] save a draft while you are typing the post --- .../amethyst/LocalPreferences.kt | 25 ++++++++++++++++ .../amethyst/ui/actions/NewPostViewModel.kt | 30 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 9edba1452..fcd09ec0a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -111,6 +111,7 @@ private object PrefKeys { const val LOGIN_WITH_EXTERNAL_SIGNER = "login_with_external_signer" const val AUTOMATICALLY_SHOW_PROFILE_PICTURE = "automatically_show_profile_picture" const val SIGNER_PACKAGE_NAME = "signer_package_name" + const val NEW_POST_DRAFT = "draft_new_post" const val ALL_ACCOUNT_INFO = "all_saved_accounts_info" const val SHARED_SETTINGS = "shared_settings" @@ -458,6 +459,30 @@ object LocalPreferences { } } + fun saveDraft( + message: String, + account: Account, + ) { + val prefs = encryptedPreferences(account.keyPair.pubKey.toNpub()) + with(prefs.edit()) { + putString(PrefKeys.NEW_POST_DRAFT, message) + apply() + } + } + + fun loadDraft(account: Account): String? { + val prefs = encryptedPreferences(account.keyPair.pubKey.toNpub()) + return prefs.getString(PrefKeys.NEW_POST_DRAFT, "") + } + + fun clearDraft(account: Account) { + val prefs = encryptedPreferences(account.keyPair.pubKey.toNpub()) + with(prefs.edit()) { + remove(PrefKeys.NEW_POST_DRAFT) + apply() + } + } + suspend fun innerLoadCurrentAccountFromEncryptedStorage(npub: String?): Account? = withContext(Dispatchers.IO) { checkNotInMainThread() 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 7e16320f4..998141a26 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 @@ -35,6 +35,7 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.fonfon.kgeohash.toGeoHash +import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note @@ -213,6 +214,14 @@ open class NewPostViewModel() : ViewModel() { zapRaiserAmount = null forwardZapTo = Split() forwardZapToEditting = TextFieldValue("") + + viewModelScope.launch(Dispatchers.IO) { + val draft = loadDraft() + if (draft != null) { + message = TextFieldValue(draft) + updateMessage(message) + } + } } fun sendPost(relayList: List? = null) { @@ -535,6 +544,10 @@ open class NewPostViewModel() : ViewModel() { userSuggestionAnchor = null userSuggestionsMainMessage = null + viewModelScope.launch(Dispatchers.IO) { + clearDraft() + } + NostrSearchEventOrUserDataSource.clear() } @@ -550,7 +563,24 @@ open class NewPostViewModel() : ViewModel() { pTags = pTags?.filter { it != userToRemove } } + open fun saveDraft(message: String) { + account?.let { LocalPreferences.saveDraft(message, it) } + } + + open fun loadDraft(): String? { + account?.let { return LocalPreferences.loadDraft(it) } + + return null + } + + open fun clearDraft() { + account?.let { LocalPreferences.clearDraft(it) } + } + open fun updateMessage(it: TextFieldValue) { + viewModelScope.launch(Dispatchers.IO) { + saveDraft(it.text) + } message = it urlPreview = findUrlInMessage() From 76a93f84c32c5e5bd314b19db082ee634e53a04f Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Wed, 24 Jan 2024 08:21:04 -0300 Subject: [PATCH 02/32] fix default value for the draft note --- .../main/java/com/vitorpamplona/amethyst/LocalPreferences.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index fcd09ec0a..0119eacc3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -472,7 +472,7 @@ object LocalPreferences { fun loadDraft(account: Account): String? { val prefs = encryptedPreferences(account.keyPair.pubKey.toNpub()) - return prefs.getString(PrefKeys.NEW_POST_DRAFT, "") + return prefs.getString(PrefKeys.NEW_POST_DRAFT, null) } fun clearDraft(account: Account) { From ba7c59fdd5f72fdd552185f640af426aeb2d012f Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Wed, 24 Jan 2024 08:22:11 -0300 Subject: [PATCH 03/32] draft was not saving in some places --- .../amethyst/ui/actions/NewPostViewModel.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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 998141a26..329bd5d7b 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 @@ -670,6 +670,9 @@ open class NewPostViewModel() : ViewModel() { message.text.replaceRange(lastWordStart, it.end, wordToInsert), TextRange(lastWordStart + wordToInsert.length, lastWordStart + wordToInsert.length), ) + viewModelScope.launch(Dispatchers.IO) { + saveDraft(message.text) + } } else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) { forwardZapTo.addItem(item) forwardZapToEditting = TextFieldValue("") @@ -852,6 +855,9 @@ open class NewPostViewModel() : ViewModel() { ), ) urlPreview = findUrlInMessage() + viewModelScope.launch(Dispatchers.IO) { + saveDraft(message.text) + } }, onError = { isUploadingImage = false @@ -915,7 +921,10 @@ open class NewPostViewModel() : ViewModel() { isUploadingImage = false - note?.let { message = TextFieldValue(message.text + "\nnostr:" + it.toNEvent()) } + note?.let { + message = TextFieldValue(message.text + "\nnostr:" + it.toNEvent()) + saveDraft(message.text) + } urlPreview = findUrlInMessage() } From 99965ecd2d201e3c5aac0aa3e0879af1b00bf791 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Wed, 24 Jan 2024 08:22:35 -0300 Subject: [PATCH 04/32] add an edit draft in the drawer --- .../amethyst/ui/navigation/DrawerContent.kt | 142 +++++++++++++++++- app/src/main/res/values/strings.xml | 1 + 2 files changed, 135 insertions(+), 8 deletions(-) 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 457be36d0..38441e2e4 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 @@ -20,17 +20,20 @@ */ package com.vitorpamplona.amethyst.ui.navigation +import android.util.Log import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -38,6 +41,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll @@ -49,9 +53,12 @@ import androidx.compose.material3.Divider import androidx.compose.material3.DrawerState import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalDrawerSheet import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -75,16 +82,22 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction 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.text.style.TextOverflow 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.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage import com.vitorpamplona.amethyst.BuildConfig +import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.RelayPool import com.vitorpamplona.amethyst.service.relays.RelayPoolStatus +import com.vitorpamplona.amethyst.ui.actions.CloseButton import com.vitorpamplona.amethyst.ui.actions.NewRelayListView import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage @@ -155,7 +168,10 @@ fun DrawerContent( ) ListContent( - modifier = Modifier.fillMaxWidth().weight(1f), + modifier = + Modifier + .fillMaxWidth() + .weight(1f), drawerState, openSheet, accountViewModel, @@ -227,7 +243,8 @@ fun ProfileContentTemplate( model = profilePicture, contentDescription = stringResource(id = R.string.profile_image), modifier = - Modifier.width(100.dp) + Modifier + .width(100.dp) .height(100.dp) .clip(shape = CircleShape) .border(3.dp, MaterialTheme.colorScheme.background, CircleShape) @@ -240,7 +257,10 @@ fun ProfileContentTemplate( CreateTextWithEmoji( text = bestDisplayName, tags = tags, - modifier = Modifier.padding(top = 7.dp).clickable(onClick = onClick), + modifier = + Modifier + .padding(top = 7.dp) + .clickable(onClick = onClick), fontWeight = FontWeight.Bold, fontSize = 18.sp, maxLines = 1, @@ -450,8 +470,26 @@ fun ListContent( val proxyPort = remember { mutableStateOf(accountViewModel.account.proxyPort.toString()) } val context = LocalContext.current + var draftText by remember { + mutableStateOf(null) + } + + var showDraft by remember { mutableStateOf(false) } + + LaunchedEffect(drawerState.isOpen) { + if (drawerState.isOpen) { + launch(Dispatchers.IO) { + Log.d("draftText", "loading draft") + draftText = LocalPreferences.loadDraft(accountViewModel.account) + } + } + } + Column( - modifier = modifier.fillMaxHeight().verticalScroll(rememberScrollState()), + modifier = + modifier + .fillMaxHeight() + .verticalScroll(rememberScrollState()), ) { NavigationRow( title = stringResource(R.string.profile), @@ -523,6 +561,18 @@ fun ListContent( }, ) + draftText?.let { + IconRow( + title = stringResource(R.string.edit_draft), + icon = R.drawable.ic_lists, + tint = MaterialTheme.colorScheme.onBackground, + onClick = { + coroutineScope.launch { drawerState.close() } + showDraft = true + }, + ) + } + NavigationRow( title = stringResource(R.string.settings), icon = Route.Settings.icon, @@ -567,6 +617,17 @@ fun ListContent( ) } + if (showDraft) { + EditDraftDialog( + { + draftText = null + showDraft = false + }, + { }, + draftText!!, + ) + } + if (disconnectTorDialog) { AlertDialog( title = { Text(text = stringResource(R.string.do_you_really_want_to_disable_tor_title)) }, @@ -594,6 +655,61 @@ fun ListContent( } } +@Composable +fun EditDraftDialog( + onClose: () -> Unit, + onPost: () -> Unit, + draftText: String, +) { + var message by remember { + mutableStateOf(TextFieldValue(draftText)) + } + Dialog( + onDismissRequest = onClose, + properties = DialogProperties(usePlatformDefaultWidth = false), + ) { + Surface(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier.padding(10.dp), + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + CloseButton(onPress = { onClose() }) + } + + Column( + modifier = Modifier.padding(8.dp), + ) { + OutlinedTextField( + value = message, + onValueChange = { message = it }, + keyboardOptions = + KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.Sentences, + ), + modifier = + Modifier.fillMaxWidth() + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.surface, + shape = RoundedCornerShape(8.dp), + ), + colors = + OutlinedTextFieldDefaults.colors( + focusedBorderColor = Color.Transparent, + unfocusedBorderColor = Color.Transparent, + ), + textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content), + ) + } + } + } + } +} + @Composable private fun RelayStatus(accountViewModel: AccountViewModel) { val connectedRelaysText by RelayPool.statusFlow.collectAsStateWithLifecycle(RelayPoolStatus(0, 0)) @@ -658,7 +774,8 @@ fun IconRow( ) { Row( modifier = - Modifier.fillMaxWidth() + Modifier + .fillMaxWidth() .combinedClickable( onClick = onClick, onLongClick = onLongClick, @@ -689,10 +806,16 @@ fun IconRowRelays( onClick: () -> Unit, ) { Row( - modifier = Modifier.fillMaxWidth().clickable { onClick() }, + modifier = + Modifier + .fillMaxWidth() + .clickable { onClick() }, ) { Row( - modifier = Modifier.fillMaxWidth().padding(vertical = 15.dp, horizontal = 25.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 15.dp, horizontal = 25.dp), verticalAlignment = Alignment.CenterVertically, ) { Icon( @@ -733,7 +856,10 @@ fun BottomContent( thickness = DividerThickness, ) Row( - modifier = Modifier.fillMaxWidth().padding(horizontal = 15.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 15.dp), verticalAlignment = Alignment.CenterVertically, ) { Text( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ee7f9a7fe..8d272c4f0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -701,4 +701,5 @@ Server did not provide a URL after uploading Could not download uploaded media from the server Could not prepare local file to upload: %1$s + Edit draft From 2c086f76e2afaf8b1473f67ade924cee96bbb120 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Wed, 24 Jan 2024 08:40:55 -0300 Subject: [PATCH 05/32] open the post screen after editing the text --- .../amethyst/ui/navigation/DrawerContent.kt | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) 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 38441e2e4..62c1cd68c 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 @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.navigation -import android.util.Log import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -98,7 +97,9 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.RelayPool import com.vitorpamplona.amethyst.service.relays.RelayPoolStatus import com.vitorpamplona.amethyst.ui.actions.CloseButton +import com.vitorpamplona.amethyst.ui.actions.NewPostView import com.vitorpamplona.amethyst.ui.actions.NewRelayListView +import com.vitorpamplona.amethyst.ui.actions.PostButton import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.note.LoadStatuses @@ -475,11 +476,11 @@ fun ListContent( } var showDraft by remember { mutableStateOf(false) } + var wantsToPost by remember { mutableStateOf(false) } LaunchedEffect(drawerState.isOpen) { if (drawerState.isOpen) { launch(Dispatchers.IO) { - Log.d("draftText", "loading draft") draftText = LocalPreferences.loadDraft(accountViewModel.account) } } @@ -617,13 +618,32 @@ fun ListContent( ) } + if (wantsToPost) { + NewPostView( + { + wantsToPost = false + draftText = null + coroutineScope.launch { drawerState.close() } + }, + accountViewModel = accountViewModel, + nav = nav, + ) + } + if (showDraft) { EditDraftDialog( { draftText = null showDraft = false }, - { }, + { + coroutineScope.launch(Dispatchers.IO) { + LocalPreferences.saveDraft(it, accountViewModel.account) + draftText = null + showDraft = false + wantsToPost = true + } + }, draftText!!, ) } @@ -658,7 +678,7 @@ fun ListContent( @Composable fun EditDraftDialog( onClose: () -> Unit, - onPost: () -> Unit, + onPost: (String) -> Unit, draftText: String, ) { var message by remember { @@ -678,6 +698,7 @@ fun EditDraftDialog( modifier = Modifier.fillMaxWidth(), ) { CloseButton(onPress = { onClose() }) + PostButton(isActive = true, onPost = { onPost(message.text) }) } Column( From cdd620987b45aa75ccf7a4f179afb438e347e960 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Wed, 24 Jan 2024 09:55:31 -0300 Subject: [PATCH 06/32] implement reply draft --- .../vitorpamplona/amethyst/LocalPreferences.kt | 9 +++++++++ .../amethyst/ui/actions/NewPostView.kt | 16 ++++++++++++++-- .../amethyst/ui/actions/NewPostViewModel.kt | 4 +++- .../amethyst/ui/navigation/DrawerContent.kt | 2 +- 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 0119eacc3..e0f65780d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -112,6 +112,7 @@ private object PrefKeys { const val AUTOMATICALLY_SHOW_PROFILE_PICTURE = "automatically_show_profile_picture" const val SIGNER_PACKAGE_NAME = "signer_package_name" const val NEW_POST_DRAFT = "draft_new_post" + const val DRAFT_REPLY_POST = "draft_reply_post" const val ALL_ACCOUNT_INFO = "all_saved_accounts_info" const val SHARED_SETTINGS = "shared_settings" @@ -461,15 +462,22 @@ object LocalPreferences { fun saveDraft( message: String, + replyPost: String?, account: Account, ) { val prefs = encryptedPreferences(account.keyPair.pubKey.toNpub()) with(prefs.edit()) { putString(PrefKeys.NEW_POST_DRAFT, message) + putString(PrefKeys.DRAFT_REPLY_POST, replyPost) apply() } } + fun loadReplyDraft(account: Account): String? { + val prefs = encryptedPreferences(account.keyPair.pubKey.toNpub()) + return prefs.getString(PrefKeys.DRAFT_REPLY_POST, null) + } + fun loadDraft(account: Account): String? { val prefs = encryptedPreferences(account.keyPair.pubKey.toNpub()) return prefs.getString(PrefKeys.NEW_POST_DRAFT, null) @@ -478,6 +486,7 @@ object LocalPreferences { fun clearDraft(account: Account) { val prefs = encryptedPreferences(account.keyPair.pubKey.toNpub()) with(prefs.edit()) { + remove(PrefKeys.DRAFT_REPLY_POST) remove(PrefKeys.NEW_POST_DRAFT) apply() } 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 22b8b5a8a..9fa061371 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 @@ -130,7 +130,9 @@ import com.fonfon.kgeohash.toGeoHash import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState +import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.Nip96MediaServers @@ -207,9 +209,19 @@ fun NewPostView( var relayList = remember { accountViewModel.account.activeWriteRelays().toImmutableList() } LaunchedEffect(Unit) { - postViewModel.load(accountViewModel, baseReplyTo, quote) - launch(Dispatchers.IO) { + val replyDraft = LocalPreferences.loadReplyDraft(accountViewModel.account) + if (replyDraft.isNullOrBlank()) { + postViewModel.load(accountViewModel, baseReplyTo, quote) + } else { + val note = LocalCache.checkGetOrCreateNote(replyDraft) + if (note == null) { + postViewModel.load(accountViewModel, baseReplyTo, quote) + } else { + postViewModel.load(accountViewModel, note, quote) + } + } + postViewModel.imageUploadingError.collect { error -> withContext(Dispatchers.Main) { Toast.makeText(context, error, Toast.LENGTH_SHORT).show() } } 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 329bd5d7b..17482fd0a 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 @@ -515,6 +515,7 @@ open class NewPostViewModel() : ViewModel() { urlPreview = null isUploadingImage = false pTags = null + eTags = null wantsDirectMessage = false @@ -543,6 +544,7 @@ open class NewPostViewModel() : ViewModel() { userSuggestions = emptyList() userSuggestionAnchor = null userSuggestionsMainMessage = null + originalNote = null viewModelScope.launch(Dispatchers.IO) { clearDraft() @@ -564,7 +566,7 @@ open class NewPostViewModel() : ViewModel() { } open fun saveDraft(message: String) { - account?.let { LocalPreferences.saveDraft(message, it) } + account?.let { LocalPreferences.saveDraft(message, originalNote?.idHex, it) } } open fun loadDraft(): String? { 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 62c1cd68c..c01291854 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 @@ -638,7 +638,7 @@ fun ListContent( }, { coroutineScope.launch(Dispatchers.IO) { - LocalPreferences.saveDraft(it, accountViewModel.account) + LocalPreferences.saveDraft(it, null, accountViewModel.account) draftText = null showDraft = false wantsToPost = true From 4d2c17cd1cd9d1d15896a686abb03beb2e58ac3c Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Wed, 28 Feb 2024 11:16:19 -0300 Subject: [PATCH 07/32] fix draft on nip94 --- .../java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt | 2 +- .../com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) 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 abcb99045..ee8aeb49e 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 @@ -130,8 +130,8 @@ import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.commons.RichTextParser +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.Nip96MediaServers 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 1a06d7768..19920809a 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 @@ -843,6 +843,7 @@ open class NewPostViewModel() : ViewModel() { nip94attachments = nip94attachments + event message = message.insertUrlAtCursor(imageUrl) + saveDraft(message.text) urlPreview = findUrlInMessage() } }, From fa5d992010b180850892ebe374ef99c56c5abf03 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Wed, 13 Mar 2024 13:24:22 -0300 Subject: [PATCH 08/32] add draftevent class --- .../vitorpamplona/quartz/events/DraftEvent.kt | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/DraftEvent.kt diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/DraftEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/DraftEvent.kt new file mode 100644 index 000000000..027dbdb4a --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/DraftEvent.kt @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.events + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class DraftEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { + companion object { + const val KIND = 31234 + } + + fun create( + dTag: String, + originalNote: EventInterface, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (DraftEvent) -> Unit, + ) { + val tags = + arrayOf( + arrayOf("d", dTag), + arrayOf("k", "${originalNote.kind()}"), + ) + + signer.nip44Encrypt(originalNote.content(), signer.pubKey) { + signer.sign(createdAt, KIND, tags, it, onReady) + } + } +} From e292affbe614d492df4cddcd89b14b637fc330d9 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Fri, 15 Mar 2024 09:08:35 -0300 Subject: [PATCH 09/32] add draft in the home feed --- .../vitorpamplona/amethyst/model/Account.kt | 35 +++-- .../amethyst/model/LocalCache.kt | 9 ++ .../com/vitorpamplona/amethyst/model/Note.kt | 12 ++ .../service/NostrAccountDataSource.kt | 54 +++++-- .../amethyst/service/relays/Relay.kt | 2 +- .../amethyst/ui/actions/NewPostViewModel.kt | 52 +++++-- .../ui/dal/HomeConversationsFeedFilter.kt | 4 +- .../amethyst/ui/note/NoteCompose.kt | 20 +++ .../ui/note/elements/DisplayReward.kt | 1 + .../amethyst/ui/screen/ThreadFeedView.kt | 5 + .../vitorpamplona/quartz/events/DraftEvent.kt | 137 +++++++++++++++--- .../quartz/events/EventFactory.kt | 1 + .../quartz/events/NIP24Factory.kt | 46 ------ .../quartz/events/TextNoteEvent.kt | 3 +- .../quartz/signers/NostrSigner.kt | 1 + .../quartz/signers/NostrSignerExternal.kt | 28 ++++ .../quartz/signers/NostrSignerInternal.kt | 30 ++++ 17 files changed, 334 insertions(+), 106 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 ffcfd11db..667a7507c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -56,6 +56,7 @@ import com.vitorpamplona.quartz.events.ClassifiedsEvent import com.vitorpamplona.quartz.events.Contact import com.vitorpamplona.quartz.events.ContactListEvent import com.vitorpamplona.quartz.events.DeletionEvent +import com.vitorpamplona.quartz.events.DraftEvent import com.vitorpamplona.quartz.events.EmojiPackEvent import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent import com.vitorpamplona.quartz.events.EmojiUrl @@ -1422,6 +1423,7 @@ class Account( relayList: List? = null, geohash: String? = null, nip94attachments: List? = null, + draftTag: String?, ) { if (!isWriteable()) return @@ -1445,20 +1447,28 @@ class Account( nip94attachments = nip94attachments, forkedFrom = forkedFrom, signer = signer, + isDraft = draftTag != null, ) { - Client.send(it, relayList = relayList) - LocalCache.justConsume(it, null) - - // broadcast replied notes - replyingTo?.let { - LocalCache.getNoteIfExists(replyingTo)?.event?.let { - Client.send(it, relayList = relayList) + if (draftTag != null) { + DraftEvent.create(draftTag, it, signer) { draftEvent -> + Client.send(draftEvent, relayList = relayList) + LocalCache.justConsume(draftEvent, null) } - } - replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } } - addresses?.forEach { - LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let { - Client.send(it, relayList = relayList) + } else { + Client.send(it, relayList = relayList) + LocalCache.justConsume(it, null) + + // broadcast replied notes + replyingTo?.let { + LocalCache.getNoteIfExists(replyingTo)?.event?.let { + Client.send(it, relayList = relayList) + } + } + replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } } + addresses?.forEach { + LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let { + Client.send(it, relayList = relayList) + } } } } @@ -2210,6 +2220,7 @@ class Account( fun cachedDecryptContent(note: Note): String? { val event = note.event + return if (event is PrivateDmEvent && isWriteable()) { event.cachedContentFor(signer) } else if (event is LnZapRequestEvent && event.isPrivateZap() && isWriteable()) { 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 f211e30f4..ee68d4585 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -61,6 +61,7 @@ import com.vitorpamplona.quartz.events.CommunityListEvent import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent import com.vitorpamplona.quartz.events.ContactListEvent import com.vitorpamplona.quartz.events.DeletionEvent +import com.vitorpamplona.quartz.events.DraftEvent import com.vitorpamplona.quartz.events.EmojiPackEvent import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent import com.vitorpamplona.quartz.events.Event @@ -2042,6 +2043,13 @@ object LocalCache { } } + private fun consume( + event: DraftEvent, + relay: Relay?, + ) { + consumeBaseReplaceable(event, relay) + } + fun justConsume( event: Event, relay: Relay?, @@ -2079,6 +2087,7 @@ object LocalCache { } is ContactListEvent -> consume(event) is DeletionEvent -> consume(event) + is DraftEvent -> consume(event, relay) is EmojiPackEvent -> consume(event, relay) is EmojiPackSelectionEvent -> consume(event, relay) is SealedGossipEvent -> consume(event, relay) 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 77f380b3c..b4523c9c6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -106,6 +106,7 @@ open class Note(val idHex: String) { var event: EventInterface? = null var author: User? = null var replyTo: List? = null + var draft: String? = null // These fields are updated every time an event related to this note is received. var replies = listOf() @@ -183,6 +184,17 @@ open class Note(val idHex: String) { open fun createdAt() = event?.createdAt() + fun updateDraft(id: String) { + draft = id + } + + fun isDraft(): Boolean { + draft?.let { + return it.isNotBlank() + } + return false + } + fun loadEvent( event: Event, author: User, 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 0e49e30bf..77288c2d9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt @@ -40,6 +40,7 @@ import com.vitorpamplona.quartz.events.CalendarRSVPEvent import com.vitorpamplona.quartz.events.CalendarTimeSlotEvent import com.vitorpamplona.quartz.events.ChannelMessageEvent import com.vitorpamplona.quartz.events.ContactListEvent +import com.vitorpamplona.quartz.events.DraftEvent import com.vitorpamplona.quartz.events.EmojiPackSelectionEvent import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.EventInterface @@ -229,6 +230,16 @@ object NostrAccountDataSource : NostrDataSource("AccountData") { ) } + fun createDraftsFilter() = + TypedFilter( + types = COMMON_FEED_TYPES, + filter = + JsonFilter( + kinds = listOf(DraftEvent.KIND), + authors = listOf(account.userProfile().pubkeyHex), + ), + ) + fun createGiftWrapsToMeFilter() = TypedFilter( types = COMMON_FEED_TYPES, @@ -262,22 +273,38 @@ object NostrAccountDataSource : NostrDataSource("AccountData") { checkNotInMainThread() if (LocalCache.justVerify(event)) { - if (event is GiftWrapEvent) { - // Avoid decrypting over and over again if the event already exist. - val note = LocalCache.getNoteIfExists(event.id) - if (note != null && relay.brief in note.relays) return + when (event) { + is DraftEvent -> { + // Avoid decrypting over and over again if the event already exist. + val note = LocalCache.getNoteIfExists(event.id) + if (note != null && relay.brief in note.relays) return - event.cachedGift(account.signer) { this.consume(it, relay) } - } + event.plainContent(account.signer) { + LocalCache.justConsume(it, relay) + val draftNote = LocalCache.getNoteIfExists(it.id) + draftNote?.updateDraft(event.id) + } + } - if (event is SealedGossipEvent) { - // Avoid decrypting over and over again if the event already exist. - val note = LocalCache.getNoteIfExists(event.id) - if (note != null && relay.brief in note.relays) return + is GiftWrapEvent -> { + // Avoid decrypting over and over again if the event already exist. + val note = LocalCache.getNoteIfExists(event.id) + if (note != null && relay.brief in note.relays) return - event.cachedGossip(account.signer) { LocalCache.justConsume(it, relay) } - } else { - LocalCache.justConsume(event, relay) + event.cachedGift(account.signer) { this.consume(it, relay) } + } + + is SealedGossipEvent -> { + // Avoid decrypting over and over again if the event already exist. + val note = LocalCache.getNoteIfExists(event.id) + if (note != null && relay.brief in note.relays) return + + event.cachedGossip(account.signer) { LocalCache.justConsume(it, relay) } + } + + else -> { + LocalCache.justConsume(event, relay) + } } } } @@ -328,6 +355,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") { createAccountSettingsFilter(), createAccountLastPostsListFilter(), createOtherAccountsBaseFilter(), + createDraftsFilter(), ) .ifEmpty { null } } else { 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 04a6aaaa2..747473bcf 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 @@ -263,7 +263,7 @@ class Relay( val subscriptionId = msgArray.get(1).asText() val event = Event.fromJson(msgArray.get(2)) - // Log.w("Relay", "Relay onEVENT ${event.kind} $url, $subscriptionId ${msgArray.get(2)}") + Log.w("Relay", "Relay onEVENT ${event.kind} $url, $subscriptionId ${msgArray.get(2)}") listeners.forEach { it.onEvent( this@Relay, 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 285a4139c..fee300398 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 @@ -74,6 +74,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.launch +import java.util.UUID enum class UserSuggestionAnchor { MAIN_MESSAGE, @@ -83,6 +84,7 @@ enum class UserSuggestionAnchor { @Stable open class NewPostViewModel() : ViewModel() { + var draftTag: String = UUID.randomUUID().toString() var accountViewModel: AccountViewModel? = null var account: Account? = null var requiresNIP24: Boolean = false @@ -306,11 +308,17 @@ open class NewPostViewModel() : ViewModel() { } } - fun sendPost(relayList: List? = null) { - viewModelScope.launch(Dispatchers.IO) { innerSendPost(relayList) } + fun sendPost( + relayList: List? = null, + localDraft: String? = null, + ) { + viewModelScope.launch(Dispatchers.IO) { innerSendPost(relayList, localDraft) } } - suspend fun innerSendPost(relayList: List? = null) { + private suspend fun innerSendPost( + relayList: List? = null, + localDraft: String?, + ) { if (accountViewModel == null) { cancel() return @@ -555,11 +563,13 @@ open class NewPostViewModel() : ViewModel() { relayList = relayList, geohash = geoHash, nip94attachments = usedAttachments, + draftTag = draftTag, ) } } - - cancel() + if (localDraft == null) { + cancel() + } } fun upload( @@ -693,8 +703,8 @@ open class NewPostViewModel() : ViewModel() { pTags = pTags?.filter { it != userToRemove } } - open fun saveDraft(message: String) { - account?.let { LocalPreferences.saveDraft(message, originalNote?.idHex, it) } + open fun saveDraft() { + sendPost(localDraft = draftTag) } open fun loadDraft(): String? { @@ -708,9 +718,6 @@ open class NewPostViewModel() : ViewModel() { } open fun updateMessage(it: TextFieldValue) { - viewModelScope.launch(Dispatchers.IO) { - saveDraft(it.text) - } message = it urlPreview = findUrlInMessage() @@ -732,6 +739,10 @@ open class NewPostViewModel() : ViewModel() { userSuggestions = emptyList() } } + + viewModelScope.launch(Dispatchers.IO) { + saveDraft() + } } open fun updateToUsers(it: TextFieldValue) { @@ -755,10 +766,16 @@ open class NewPostViewModel() : ViewModel() { userSuggestions = emptyList() } } + viewModelScope.launch(Dispatchers.IO) { + saveDraft() + } } open fun updateSubject(it: TextFieldValue) { subject = it + viewModelScope.launch(Dispatchers.IO) { + saveDraft() + } } open fun updateZapForwardTo(it: TextFieldValue) { @@ -785,6 +802,9 @@ open class NewPostViewModel() : ViewModel() { userSuggestions = emptyList() } } + viewModelScope.launch(Dispatchers.IO) { + saveDraft() + } } open fun autocompleteWithUser(item: User) { @@ -800,9 +820,6 @@ open class NewPostViewModel() : ViewModel() { message.text.replaceRange(lastWordStart, it.end, wordToInsert), TextRange(lastWordStart + wordToInsert.length, lastWordStart + wordToInsert.length), ) - viewModelScope.launch(Dispatchers.IO) { - saveDraft(message.text) - } } else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) { forwardZapTo.addItem(item) forwardZapToEditting = TextFieldValue("") @@ -833,6 +850,10 @@ open class NewPostViewModel() : ViewModel() { userSuggestionsMainMessage = null userSuggestions = emptyList() } + + viewModelScope.launch(Dispatchers.IO) { + saveDraft() + } } private fun newStateMapPollOptions(): SnapshotStateMap { @@ -902,8 +923,8 @@ open class NewPostViewModel() : ViewModel() { nip94attachments = nip94attachments + event message = message.insertUrlAtCursor(imageUrl) - saveDraft(message.text) urlPreview = findUrlInMessage() + saveDraft() } }, onError = { @@ -945,10 +966,10 @@ open class NewPostViewModel() : ViewModel() { note?.let { message = message.insertUrlAtCursor("nostr:" + it.toNEvent()) - saveDraft(message.text) } urlPreview = findUrlInMessage() + saveDraft() } }, onError = { @@ -969,6 +990,7 @@ open class NewPostViewModel() : ViewModel() { locUtil?.let { location = it.locationStateFlow.mapLatest { it.toGeoHash(GeohashPrecision.KM_5_X_5.digits).toString() } + saveDraft() } viewModelScope.launch(Dispatchers.IO) { locUtil?.start() } } 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 15d977db7..b66750632 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 @@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.quartz.events.ChannelMessageEvent +import com.vitorpamplona.quartz.events.DraftEvent import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.PeopleListEvent @@ -69,7 +70,8 @@ class HomeConversationsFeedFilter(val account: Account) : AdditiveFeedFilter>, content: String, sig: HexKey, -) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { - companion object { - const val KIND = 31234 +) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + @Transient private var decryptedContent: Map = mapOf() + + @Transient private var citedNotesCache: Set? = null + + fun replyTos(): List { + val oldStylePositional = tags.filter { it.size > 1 && it.size <= 3 && it[0] == "e" }.map { it[1] } + val newStyleReply = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "reply" }?.get(1) + val newStyleRoot = tags.lastOrNull { it.size > 3 && it[0] == "e" && it[3] == "root" }?.get(1) + + val newStyleReplyTos = listOfNotNull(newStyleReply, newStyleRoot) + + return if (newStyleReplyTos.isNotEmpty()) { + newStyleReplyTos + } else { + oldStylePositional + } } - fun create( - dTag: String, - originalNote: EventInterface, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (DraftEvent) -> Unit, - ) { - val tags = - arrayOf( - arrayOf("d", dTag), - arrayOf("k", "${originalNote.kind()}"), - ) + fun findCitations(): Set { + citedNotesCache?.let { + return it + } - signer.nip44Encrypt(originalNote.content(), signer.pubKey) { - signer.sign(createdAt, KIND, tags, it, onReady) + val citations = mutableSetOf() + // Removes citations from replies: + val matcher = tagSearch.matcher(content) + while (matcher.find()) { + try { + val tag = matcher.group(1)?.let { tags[it.toInt()] } + if (tag != null && tag.size > 1 && tag[0] == "e") { + citations.add(tag[1]) + } + if (tag != null && tag.size > 1 && tag[0] == "a") { + citations.add(tag[1]) + } + } catch (e: Exception) { + } + } + + val matcher2 = Nip19Bech32.nip19regex.matcher(content) + while (matcher2.find()) { + val type = matcher2.group(2) // npub1 + val key = matcher2.group(3) // bech32 + val additionalChars = matcher2.group(4) // additional chars + + if (type != null) { + val parsed = Nip19Bech32.parseComponents(type, key, additionalChars)?.entity + + if (parsed != null) { + when (parsed) { + is Nip19Bech32.NEvent -> citations.add(parsed.hex) + is Nip19Bech32.NAddress -> citations.add(parsed.atag) + is Nip19Bech32.Note -> citations.add(parsed.hex) + is Nip19Bech32.NEmbed -> citations.add(parsed.event.id) + } + } + } + } + + citedNotesCache = citations + return citations + } + + fun tagsWithoutCitations(): List { + val repliesTo = replyTos() + val tagAddresses = + taggedAddresses().filter { + it.kind != CommunityDefinitionEvent.KIND && + it.kind != WikiNoteEvent.KIND + }.map { it.toTag() } + if (repliesTo.isEmpty() && tagAddresses.isEmpty()) return emptyList() + + val citations = findCitations() + + return if (citations.isEmpty()) { + repliesTo + tagAddresses + } else { + repliesTo.filter { it !in citations } + } + } + + fun cachedContentFor(): Event? { + return decryptedContent[dTag()] + } + + fun plainContent( + signer: NostrSigner, + onReady: (Event) -> Unit, + ) { + decryptedContent[dTag()]?.let { + onReady(it) + return + } + + signer.nip44Decrypt(content, signer.pubKey) { retVal -> + val event = runCatching { fromJson(retVal) }.getOrNull() ?: return@nip44Decrypt + decryptedContent = decryptedContent + Pair(dTag(), event) + + onReady(event) + } + } + + companion object { + const val KIND = 31234 + + fun create( + dTag: String, + originalNote: EventInterface, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (DraftEvent) -> Unit, + ) { + val tags = mutableListOf>() + tags.add(arrayOf("d", dTag)) + tags.add(arrayOf("k", "${originalNote.kind()}")) + tags.addAll(originalNote.tags().filter { it.size > 1 && it[0] == "e" }) + tags.addAll(originalNote.tags().filter { it.size > 1 && it[0] == "a" }) + + signer.nip44Encrypt(originalNote.toJson(), signer.pubKey) { encryptedContent -> + signer.sign(createdAt, KIND, tags.toTypedArray(), encryptedContent, onReady) + } } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt index 306277d04..d923354c8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt @@ -79,6 +79,7 @@ class EventFactory { CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig) ContactListEvent.KIND -> ContactListEvent(id, pubKey, createdAt, tags, content, sig) DeletionEvent.KIND -> DeletionEvent(id, pubKey, createdAt, tags, content, sig) + DraftEvent.KIND -> DraftEvent(id, pubKey, createdAt, tags, content, sig) EmojiPackEvent.KIND -> EmojiPackEvent(id, pubKey, createdAt, tags, content, sig) EmojiPackSelectionEvent.KIND -> EmojiPackSelectionEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP24Factory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP24Factory.kt index 5ff107a50..4c623152f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP24Factory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP24Factory.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.quartz.events -import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.signers.NostrSigner @@ -155,49 +154,4 @@ class NIP24Factory { } } } - - fun createTextNoteNIP24( - msg: String, - to: List, - signer: NostrSigner, - replyTos: List? = null, - mentions: List? = null, - addresses: List?, - extraTags: List?, - zapReceiver: List? = null, - markAsSensitive: Boolean = false, - replyingTo: String?, - root: String?, - directMentions: Set, - zapRaiserAmount: Long? = null, - geohash: String? = null, - onReady: (Result) -> Unit, - ) { - val senderPublicKey = signer.pubKey - - TextNoteEvent.create( - msg = msg, - signer = signer, - replyTos = replyTos, - mentions = mentions, - zapReceiver = zapReceiver, - root = root, - extraTags = extraTags, - addresses = addresses, - directMentions = directMentions, - replyingTo = replyingTo, - markAsSensitive = markAsSensitive, - zapRaiserAmount = zapRaiserAmount, - geohash = geohash, - ) { senderMessage -> - createWraps(senderMessage, to.plus(senderPublicKey).toSet(), signer) { wraps -> - onReady( - Result( - msg = senderMessage, - wraps = wraps, - ), - ) - } - } - } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt index 13f81f043..fece5f0a5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/TextNoteEvent.kt @@ -60,6 +60,7 @@ class TextNoteEvent( forkedFrom: Event? = null, signer: NostrSigner, createdAt: Long = TimeUtils.now(), + isDraft: Boolean, onReady: (TextNoteEvent) -> Unit, ) { val tags = mutableListOf>() @@ -121,7 +122,7 @@ class TextNoteEvent( } } - signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady) + signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady, isDraft) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSigner.kt b/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSigner.kt index 5014db2f2..ea1c56aaf 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSigner.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSigner.kt @@ -32,6 +32,7 @@ abstract class NostrSigner(val pubKey: HexKey) { tags: Array>, content: String, onReady: (T) -> Unit, + isDraft: Boolean = false, ) abstract fun nip04Encrypt( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerExternal.kt b/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerExternal.kt index 4f34af999..30cf69b46 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerExternal.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerExternal.kt @@ -40,7 +40,13 @@ class NostrSignerExternal( tags: Array>, content: String, onReady: (T) -> Unit, + isDraft: Boolean, ) { + if (isDraft) { + unsignedEvent(createdAt, kind, tags, content, onReady) + return + } + val id = Event.generateId(pubKey, createdAt, kind, tags, content).toHexKey() val event = @@ -86,6 +92,28 @@ class NostrSignerExternal( } } + fun unsignedEvent( + createdAt: Long, + kind: Int, + tags: Array>, + content: String, + onReady: (T) -> Unit, + ) { + val id = Event.generateId(pubKey, createdAt, kind, tags, content) + + onReady( + EventFactory.create( + id.toHexKey(), + pubKey, + createdAt, + kind, + tags, + content, + "", + ) as T, + ) + } + override fun nip04Encrypt( decryptedContent: String, toPublicKey: HexKey, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerInternal.kt b/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerInternal.kt index 2537287b5..0df60b80d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerInternal.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerInternal.kt @@ -38,9 +38,15 @@ class NostrSignerInternal(val keyPair: KeyPair) : NostrSigner(keyPair.pubKey.toH tags: Array>, content: String, onReady: (T) -> Unit, + isDraft: Boolean, ) { if (keyPair.privKey == null) return + if (isDraft) { + unsignedEvent(createdAt, kind, tags, content, onReady) + return + } + if (isUnsignedPrivateEvent(kind, tags)) { // this is a private zap signPrivateZap(createdAt, kind, tags, content, onReady) @@ -82,6 +88,30 @@ class NostrSignerInternal(val keyPair: KeyPair) : NostrSigner(keyPair.pubKey.toH ) } + fun unsignedEvent( + createdAt: Long, + kind: Int, + tags: Array>, + content: String, + onReady: (T) -> Unit, + ) { + if (keyPair.privKey == null) return + + val id = Event.generateId(pubKey, createdAt, kind, tags, content) + + onReady( + EventFactory.create( + id.toHexKey(), + pubKey, + createdAt, + kind, + tags, + content, + "", + ) as T, + ) + } + override fun nip04Encrypt( decryptedContent: String, toPublicKey: HexKey, From 99e9514d6c2494beae56563b4fb55a3214b327b2 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Fri, 15 Mar 2024 10:38:22 -0300 Subject: [PATCH 10/32] remove drafts from shared prefs --- .../amethyst/LocalPreferences.kt | 34 ------ .../amethyst/model/LocalCache.kt | 9 ++ .../amethyst/service/relays/Relay.kt | 2 +- .../amethyst/ui/actions/NewPostView.kt | 15 +-- .../amethyst/ui/actions/NewPostViewModel.kt | 20 +--- .../amethyst/ui/navigation/DrawerContent.kt | 108 ------------------ .../ui/screen/loggedIn/AccountViewModel.kt | 5 + 7 files changed, 17 insertions(+), 176 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 11527ebc7..c613cc004 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -107,8 +107,6 @@ private object PrefKeys { const val LOGIN_WITH_EXTERNAL_SIGNER = "login_with_external_signer" const val AUTOMATICALLY_SHOW_PROFILE_PICTURE = "automatically_show_profile_picture" const val SIGNER_PACKAGE_NAME = "signer_package_name" - const val NEW_POST_DRAFT = "draft_new_post" - const val DRAFT_REPLY_POST = "draft_reply_post" const val HAS_DONATED_IN_VERSION = "has_donated_in_version" const val PENDING_ATTESTATIONS = "pending_attestations" @@ -395,38 +393,6 @@ object LocalPreferences { } } - fun saveDraft( - message: String, - replyPost: String?, - account: Account, - ) { - val prefs = encryptedPreferences(account.keyPair.pubKey.toNpub()) - with(prefs.edit()) { - putString(PrefKeys.NEW_POST_DRAFT, message) - putString(PrefKeys.DRAFT_REPLY_POST, replyPost) - apply() - } - } - - fun loadReplyDraft(account: Account): String? { - val prefs = encryptedPreferences(account.keyPair.pubKey.toNpub()) - return prefs.getString(PrefKeys.DRAFT_REPLY_POST, null) - } - - fun loadDraft(account: Account): String? { - val prefs = encryptedPreferences(account.keyPair.pubKey.toNpub()) - return prefs.getString(PrefKeys.NEW_POST_DRAFT, null) - } - - fun clearDraft(account: Account) { - val prefs = encryptedPreferences(account.keyPair.pubKey.toNpub()) - with(prefs.edit()) { - remove(PrefKeys.DRAFT_REPLY_POST) - remove(PrefKeys.NEW_POST_DRAFT) - apply() - } - } - suspend fun innerLoadCurrentAccountFromEncryptedStorage(npub: String?): Account? = withContext(Dispatchers.IO) { checkNotInMainThread() 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 ee68d4585..1054d8bf1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -155,6 +155,15 @@ object LocalCache { return null } + fun draftNotes(draftTag: String): List { + return notes.values.filter { + it.event?.tags()?.filter { it.size > 1 && it[0] == "d" }?.map { + Log.d("tag", it[1]) + it[1] + }?.firstOrNull() == draftTag + } + } + fun getOrCreateUser(key: HexKey): User { // checkNotInMainThread() 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 3e308700c..8d2e67d98 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 @@ -263,7 +263,7 @@ class Relay( val subscriptionId = msgArray.get(1).asText() val event = Event.fromJson(msgArray.get(2)) - Log.w("Relay", "Relay onEVENT ${event.kind} $url, $subscriptionId ${msgArray.get(2)}") + // Log.w("Relay", "Relay onEVENT ${event.kind} $url, $subscriptionId ${msgArray.get(2)}") listeners.forEach { it.onEvent( this@Relay, 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 0ca801538..1ffd99cf4 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 @@ -64,7 +64,6 @@ import androidx.compose.material.icons.filled.Sell import androidx.compose.material.icons.filled.ShowChart import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff -import androidx.compose.material.icons.outlined.Bolt import androidx.compose.material.icons.rounded.Warning import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -125,10 +124,8 @@ import coil.compose.AsyncImage import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState -import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.RichTextParser -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.Nip96MediaServers @@ -204,17 +201,7 @@ fun NewPostView( LaunchedEffect(Unit) { launch(Dispatchers.IO) { - val replyDraft = LocalPreferences.loadReplyDraft(accountViewModel.account) - if (replyDraft.isNullOrBlank()) { - postViewModel.load(accountViewModel, baseReplyTo, quote, fork, version) - } else { - val note = LocalCache.checkGetOrCreateNote(replyDraft) - if (note == null) { - postViewModel.load(accountViewModel, baseReplyTo, quote, fork, version) - } else { - postViewModel.load(accountViewModel, note, quote, fork, version) - } - } + postViewModel.load(accountViewModel, baseReplyTo, quote, fork, version) postViewModel.imageUploadingError.collect { error -> withContext(Dispatchers.Main) { Toast.makeText(context, error, Toast.LENGTH_SHORT).show() } 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 d9739c918..7fd552211 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 @@ -35,7 +35,6 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.fonfon.kgeohash.toGeoHash -import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.commons.RichTextParser import com.vitorpamplona.amethyst.commons.insertUrlAtCursor import com.vitorpamplona.amethyst.model.Account @@ -229,13 +228,6 @@ open class NewPostViewModel() : ViewModel() { forwardZapTo = Split() forwardZapToEditting = TextFieldValue("") - viewModelScope.launch(Dispatchers.IO) { - val draft = loadDraft() - if (draft != null) { - message = TextFieldValue(draft) - updateMessage(message) - } - } quote?.let { message = TextFieldValue(message.text + "\nnostr:${it.toNEvent()}") urlPreview = findUrlInMessage() @@ -685,7 +677,7 @@ open class NewPostViewModel() : ViewModel() { originalNote = null viewModelScope.launch(Dispatchers.IO) { - clearDraft() + accountViewModel?.deleteDraft(draftTag) } NostrSearchEventOrUserDataSource.clear() @@ -707,16 +699,6 @@ open class NewPostViewModel() : ViewModel() { sendPost(localDraft = draftTag) } - open fun loadDraft(): String? { - account?.let { return LocalPreferences.loadDraft(it) } - - return null - } - - open fun clearDraft() { - account?.let { LocalPreferences.clearDraft(it) } - } - open fun updateMessage(it: TextFieldValue) { message = it urlPreview = findUrlInMessage() 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 d5f739cda..7a3496f75 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 @@ -26,13 +26,11 @@ import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -40,7 +38,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll @@ -52,12 +49,9 @@ import androidx.compose.material3.DrawerState import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalDrawerSheet import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -83,26 +77,19 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction 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.text.style.TextOverflow import androidx.compose.ui.text.withStyle 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.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage import com.vitorpamplona.amethyst.BuildConfig -import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.RelayPool import com.vitorpamplona.amethyst.service.relays.RelayPoolStatus -import com.vitorpamplona.amethyst.ui.actions.CloseButton import com.vitorpamplona.amethyst.ui.actions.NewPostView import com.vitorpamplona.amethyst.ui.actions.NewRelayListView -import com.vitorpamplona.amethyst.ui.actions.PostButton import com.vitorpamplona.amethyst.ui.components.ClickableText import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage @@ -479,17 +466,8 @@ fun ListContent( mutableStateOf(null) } - var showDraft by remember { mutableStateOf(false) } var wantsToPost by remember { mutableStateOf(false) } - LaunchedEffect(drawerState.isOpen) { - if (drawerState.isOpen) { - launch(Dispatchers.IO) { - draftText = LocalPreferences.loadDraft(accountViewModel.account) - } - } - } - Column( modifier = modifier @@ -566,18 +544,6 @@ fun ListContent( }, ) - draftText?.let { - IconRow( - title = stringResource(R.string.edit_draft), - icon = R.drawable.ic_lists, - tint = MaterialTheme.colorScheme.onBackground, - onClick = { - coroutineScope.launch { drawerState.close() } - showDraft = true - }, - ) - } - NavigationRow( title = stringResource(R.string.settings), icon = Route.Settings.icon, @@ -634,24 +600,6 @@ fun ListContent( ) } - if (showDraft) { - EditDraftDialog( - { - draftText = null - showDraft = false - }, - { - coroutineScope.launch(Dispatchers.IO) { - LocalPreferences.saveDraft(it, null, accountViewModel.account) - draftText = null - showDraft = false - wantsToPost = true - } - }, - draftText!!, - ) - } - if (disconnectTorDialog) { AlertDialog( title = { Text(text = stringResource(R.string.do_you_really_want_to_disable_tor_title)) }, @@ -679,62 +627,6 @@ fun ListContent( } } -@Composable -fun EditDraftDialog( - onClose: () -> Unit, - onPost: (String) -> Unit, - draftText: String, -) { - var message by remember { - mutableStateOf(TextFieldValue(draftText)) - } - Dialog( - onDismissRequest = onClose, - properties = DialogProperties(usePlatformDefaultWidth = false), - ) { - Surface(modifier = Modifier.fillMaxSize()) { - Column( - modifier = Modifier.padding(10.dp), - ) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), - ) { - CloseButton(onPress = { onClose() }) - PostButton(isActive = true, onPost = { onPost(message.text) }) - } - - Column( - modifier = Modifier.padding(8.dp), - ) { - OutlinedTextField( - value = message, - onValueChange = { message = it }, - keyboardOptions = - KeyboardOptions.Default.copy( - capitalization = KeyboardCapitalization.Sentences, - ), - modifier = - Modifier.fillMaxWidth() - .border( - width = 1.dp, - color = MaterialTheme.colorScheme.surface, - shape = RoundedCornerShape(8.dp), - ), - colors = - OutlinedTextFieldDefaults.colors( - focusedBorderColor = Color.Transparent, - unfocusedBorderColor = Color.Transparent, - ), - textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content), - ) - } - } - } - } -} - @Composable private fun RelayStatus(accountViewModel: AccountViewModel) { val connectedRelaysText by RelayPool.statusFlow.collectAsStateWithLifecycle(RelayPoolStatus(0, 0)) 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 88ec1d47b..303bb93a5 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 @@ -1318,6 +1318,11 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View } } } + + suspend fun deleteDraft(draftTag: String) { + val notes = LocalCache.draftNotes(draftTag) + account.delete(notes) + } } class HasNotificationDot(bottomNavigationItems: ImmutableList) { From f3f8bc1b654970fd47400b1cf0895b7add88b1e1 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Fri, 15 Mar 2024 10:45:53 -0300 Subject: [PATCH 11/32] show draft in the simplified view --- .../amethyst/ui/note/NoteCompose.kt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 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 1cfcef8ae..42811efb3 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 @@ -115,9 +115,9 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer -import com.vitorpamplona.amethyst.ui.theme.Font14SP import com.vitorpamplona.amethyst.ui.theme.HalfDoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.HalfPadding +import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding import com.vitorpamplona.amethyst.ui.theme.Size25dp import com.vitorpamplona.amethyst.ui.theme.Size30Modifier import com.vitorpamplona.amethyst.ui.theme.Size34dp @@ -131,10 +131,10 @@ import com.vitorpamplona.amethyst.ui.theme.WidthAuthorPictureModifier import com.vitorpamplona.amethyst.ui.theme.boostedNoteModifier import com.vitorpamplona.amethyst.ui.theme.channelNotePictureModifier import com.vitorpamplona.amethyst.ui.theme.grayText -import com.vitorpamplona.amethyst.ui.theme.lessImportantLink import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor import com.vitorpamplona.amethyst.ui.theme.normalNoteModifier import com.vitorpamplona.amethyst.ui.theme.normalWithTopMarginNoteModifier +import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.replyBackground import com.vitorpamplona.amethyst.ui.theme.replyModifier import com.vitorpamplona.quartz.events.AppDefinitionEvent @@ -1113,11 +1113,6 @@ fun SecondUserInfoRow( DisplayPoW(pow) } - if (note.isDraft()) { - Spacer(StdHorzSpacer) - DisplayDraft() - } - DisplayOts(note, accountViewModel) } } @@ -1126,10 +1121,10 @@ fun SecondUserInfoRow( fun DisplayDraft() { Text( "Draft", - color = MaterialTheme.colorScheme.lessImportantLink, - fontSize = Font14SP, fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.placeholderText, maxLines = 1, + modifier = HalfStartPadding, ) } @@ -1175,6 +1170,11 @@ fun FirstUserInfoRow( } } + if (baseNote.isDraft()) { + Spacer(StdHorzSpacer) + DisplayDraft() + } + TimeAgo(baseNote) MoreOptionsButton(baseNote, editState, accountViewModel, nav) From 2bc2890d0855df36d68fdab8648dd0bb627282db Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Fri, 15 Mar 2024 12:35:14 -0300 Subject: [PATCH 12/32] add draft support for other event kinds --- .../vitorpamplona/amethyst/model/Account.kt | 118 +++++++++++++----- .../com/vitorpamplona/amethyst/model/Note.kt | 9 +- .../service/NostrAccountDataSource.kt | 2 - .../amethyst/ui/actions/NewPostViewModel.kt | 9 +- .../amethyst/ui/note/NoteCompose.kt | 1 - .../ui/screen/loggedIn/ChannelScreen.kt | 2 + .../ui/screen/loggedIn/ChatroomScreen.kt | 1 + .../quartz/events/ChannelMessageEvent.kt | 3 +- .../quartz/events/ClassifiedsEvent.kt | 3 +- .../quartz/events/GitReplyEvent.kt | 3 +- .../events/LiveActivitiesChatMessageEvent.kt | 3 +- .../quartz/events/PollNoteEvent.kt | 3 +- .../quartz/events/PrivateDmEvent.kt | 3 +- 13 files changed, 112 insertions(+), 48 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 667a7507c..1e59b607b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -1311,6 +1311,7 @@ class Account( relayList: List? = null, geohash: String? = null, nip94attachments: List? = null, + draftTag: String?, ) { if (!isWriteable()) return @@ -1338,14 +1339,22 @@ class Account( geohash = geohash, nip94attachments = nip94attachments, signer = signer, + isDraft = draftTag != null, ) { - Client.send(it, relayList = relayList) - LocalCache.justConsume(it, null) + if (draftTag != null) { + DraftEvent.create(draftTag, it, signer) { draftEvent -> + Client.send(draftEvent, relayList = relayList) + LocalCache.justConsume(draftEvent, null) + } + } else { + Client.send(it, relayList = relayList) + LocalCache.justConsume(it, null) - replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } } - addresses?.forEach { - LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let { - Client.send(it, relayList = relayList) + replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } } + addresses?.forEach { + LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let { + Client.send(it, relayList = relayList) + } } } } @@ -1366,6 +1375,7 @@ class Account( relayList: List? = null, geohash: String? = null, nip94attachments: List? = null, + draftTag: String?, ) { if (!isWriteable()) return @@ -1389,20 +1399,28 @@ class Account( nip94attachments = nip94attachments, forkedFrom = forkedFrom, signer = signer, + isDraft = draftTag != null, ) { - Client.send(it, relayList = relayList) - LocalCache.justConsume(it, null) - - // broadcast replied notes - replyingTo?.let { - LocalCache.getNoteIfExists(replyingTo)?.event?.let { - Client.send(it, relayList = relayList) + if (draftTag != null) { + DraftEvent.create(draftTag, it, signer) { draftEvent -> + Client.send(draftEvent, relayList = relayList) + LocalCache.justConsume(draftEvent, null) } - } - replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } } - addresses?.forEach { - LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let { - Client.send(it, relayList = relayList) + } else { + Client.send(it, relayList = relayList) + LocalCache.justConsume(it, null) + + // broadcast replied notes + replyingTo?.let { + LocalCache.getNoteIfExists(replyingTo)?.event?.let { + Client.send(it, relayList = relayList) + } + } + replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } } + addresses?.forEach { + LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let { + Client.send(it, relayList = relayList) + } } } } @@ -1512,6 +1530,7 @@ class Account( relayList: List? = null, geohash: String? = null, nip94attachments: List? = null, + draftTag: String?, ) { if (!isWriteable()) return @@ -1535,15 +1554,23 @@ class Account( zapRaiserAmount = zapRaiserAmount, geohash = geohash, nip94attachments = nip94attachments, + isDraft = draftTag != null, ) { - Client.send(it, relayList = relayList) - LocalCache.justConsume(it, null) + if (draftTag != null) { + DraftEvent.create(draftTag, it, signer) { draftEvent -> + Client.send(draftEvent, relayList = relayList) + LocalCache.justConsume(draftEvent, null) + } + } else { + Client.send(it, relayList = relayList) + LocalCache.justConsume(it, null) - // Rebroadcast replies and tags to the current relay set - replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } } - addresses?.forEach { - LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let { - Client.send(it, relayList = relayList) + // Rebroadcast replies and tags to the current relay set + replyTo?.forEach { it.event?.let { Client.send(it, relayList = relayList) } } + addresses?.forEach { + LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let { + Client.send(it, relayList = relayList) + } } } } @@ -1559,6 +1586,7 @@ class Account( zapRaiserAmount: Long? = null, geohash: String? = null, nip94attachments: List? = null, + draftTag: String?, ) { if (!isWriteable()) return @@ -1576,9 +1604,17 @@ class Account( geohash = geohash, nip94attachments = nip94attachments, signer = signer, + isDraft = draftTag != null, ) { - Client.send(it) - LocalCache.justConsume(it, null) + if (draftTag != null) { + DraftEvent.create(draftTag, it, signer) { draftEvent -> + Client.send(draftEvent) + LocalCache.justConsume(draftEvent, null) + } + } else { + Client.send(it) + LocalCache.justConsume(it, null) + } } } @@ -1592,6 +1628,7 @@ class Account( zapRaiserAmount: Long? = null, geohash: String? = null, nip94attachments: List? = null, + draftTag: String?, ) { if (!isWriteable()) return @@ -1610,9 +1647,17 @@ class Account( geohash = geohash, nip94attachments = nip94attachments, signer = signer, + isDraft = draftTag != null, ) { - Client.send(it) - LocalCache.justConsume(it, null) + if (draftTag != null) { + DraftEvent.create(draftTag, it, signer) { draftEvent -> + Client.send(draftEvent) + LocalCache.justConsume(draftEvent, null) + } + } else { + Client.send(it) + LocalCache.justConsume(it, null) + } } } @@ -1626,6 +1671,7 @@ class Account( zapRaiserAmount: Long? = null, geohash: String? = null, nip94attachments: List? = null, + draftTag: String?, ) { sendPrivateMessage( message, @@ -1637,6 +1683,7 @@ class Account( zapRaiserAmount, geohash, nip94attachments, + draftTag, ) } @@ -1650,6 +1697,7 @@ class Account( zapRaiserAmount: Long? = null, geohash: String? = null, nip94attachments: List? = null, + draftTag: String?, ) { if (!isWriteable()) return @@ -1669,9 +1717,17 @@ class Account( nip94attachments = nip94attachments, signer = signer, advertiseNip18 = false, + isDraft = draftTag != null, ) { - Client.send(it) - LocalCache.consume(it, null) + if (draftTag != null) { + DraftEvent.create(draftTag, it, signer) { draftEvent -> + Client.send(draftEvent) + LocalCache.justConsume(draftEvent, null) + } + } else { + Client.send(it) + LocalCache.consume(it, null) + } } } 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 b4523c9c6..60cfeca1e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -106,7 +106,6 @@ open class Note(val idHex: String) { var event: EventInterface? = null var author: User? = null var replyTo: List? = null - var draft: String? = null // These fields are updated every time an event related to this note is received. var replies = listOf() @@ -184,13 +183,9 @@ open class Note(val idHex: String) { open fun createdAt() = event?.createdAt() - fun updateDraft(id: String) { - draft = id - } - fun isDraft(): Boolean { - draft?.let { - return it.isNotBlank() + event?.let { + return it.sig().isBlank() } return false } 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 77288c2d9..f0067ed74 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt @@ -281,8 +281,6 @@ object NostrAccountDataSource : NostrDataSource("AccountData") { event.plainContent(account.signer) { LocalCache.justConsume(it, relay) - val draftNote = LocalCache.getNoteIfExists(it.id) - draftNote?.updateDraft(event.id) } } 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 7fd552211..a8a446f5f 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 @@ -371,6 +371,7 @@ open class NewPostViewModel() : ViewModel() { zapRaiserAmount = localZapRaiserAmount, geohash = geoHash, nip94attachments = usedAttachments, + draftTag = localDraft, ) } else { account?.sendChannelMessage( @@ -383,6 +384,7 @@ open class NewPostViewModel() : ViewModel() { zapRaiserAmount = localZapRaiserAmount, geohash = geoHash, nip94attachments = usedAttachments, + draftTag = localDraft, ) } } else if (originalNote?.event is PrivateDmEvent) { @@ -396,6 +398,7 @@ open class NewPostViewModel() : ViewModel() { zapRaiserAmount = localZapRaiserAmount, geohash = geoHash, nip94attachments = usedAttachments, + draftTag = localDraft, ) } else if (originalNote?.event is ChatMessageEvent) { val receivers = @@ -443,6 +446,7 @@ open class NewPostViewModel() : ViewModel() { zapRaiserAmount = localZapRaiserAmount, geohash = geoHash, nip94attachments = usedAttachments, + draftTag = localDraft, ) } } else if (originalNote?.event is GitIssueEvent) { @@ -483,6 +487,7 @@ open class NewPostViewModel() : ViewModel() { relayList = relayList, geohash = geoHash, nip94attachments = usedAttachments, + draftTag = localDraft, ) } else { if (wantsPoll) { @@ -501,6 +506,7 @@ open class NewPostViewModel() : ViewModel() { relayList, geoHash, nip94attachments = usedAttachments, + draftTag = localDraft, ) } else if (wantsProduct) { account?.sendClassifieds( @@ -519,6 +525,7 @@ open class NewPostViewModel() : ViewModel() { relayList = relayList, geohash = geoHash, nip94attachments = usedAttachments, + draftTag = localDraft, ) } else { // adds markers @@ -555,7 +562,7 @@ open class NewPostViewModel() : ViewModel() { relayList = relayList, geohash = geoHash, nip94attachments = usedAttachments, - draftTag = draftTag, + draftTag = localDraft, ) } } 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 42811efb3..9da8f3202 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 @@ -1171,7 +1171,6 @@ fun FirstUserInfoRow( } if (baseNote.isDraft()) { - Spacer(StdHorzSpacer) DisplayDraft() } 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 91703a58e..d7313e466 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 @@ -325,6 +325,7 @@ fun ChannelScreen( mentions = tagger.pTags, wantsToMarkAsSensitive = false, nip94attachments = usedAttachments, + draftTag = null, ) } else if (channel is LiveActivitiesChannel) { accountViewModel.account.sendLiveMessage( @@ -334,6 +335,7 @@ fun ChannelScreen( mentions = tagger.pTags, wantsToMarkAsSensitive = false, nip94attachments = usedAttachments, + draftTag = null, ) } newPostModel.message = TextFieldValue("") 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 048738d96..76f3c98b0 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 @@ -348,6 +348,7 @@ fun ChatroomScreen( mentions = null, wantsToMarkAsSensitive = false, nip94attachments = usedAttachments, + draftTag = null, ) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMessageEvent.kt index c844be2d8..7000f296c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChannelMessageEvent.kt @@ -60,6 +60,7 @@ class ChannelMessageEvent( zapRaiserAmount: Long?, geohash: String? = null, nip94attachments: List? = null, + isDraft: Boolean, onReady: (ChannelMessageEvent) -> Unit, ) { val tags = @@ -87,7 +88,7 @@ class ChannelMessageEvent( arrayOf("alt", ALT), ) - signer.sign(createdAt, KIND, tags.toTypedArray(), message, onReady) + signer.sign(createdAt, KIND, tags.toTypedArray(), message, onReady, isDraft) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ClassifiedsEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ClassifiedsEvent.kt index 5b8597400..2d079eb57 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ClassifiedsEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ClassifiedsEvent.kt @@ -93,6 +93,7 @@ class ClassifiedsEvent( nip94attachments: List? = null, signer: NostrSigner, createdAt: Long = TimeUtils.now(), + isDraft: Boolean, onReady: (ClassifiedsEvent) -> Unit, ) { val tags = mutableListOf>() @@ -172,7 +173,7 @@ class ClassifiedsEvent( } tags.add(arrayOf("alt", ALT)) - signer.sign(createdAt, KIND, tags.toTypedArray(), message, onReady) + signer.sign(createdAt, KIND, tags.toTypedArray(), message, onReady, isDraft) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GitReplyEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GitReplyEvent.kt index f083c895f..88d4f5eac 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GitReplyEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GitReplyEvent.kt @@ -94,6 +94,7 @@ class GitReplyEvent( forkedFrom: Event? = null, signer: NostrSigner, createdAt: Long = TimeUtils.now(), + isDraft: Boolean, onReady: (GitReplyEvent) -> Unit, ) { val tags = mutableListOf>() @@ -156,7 +157,7 @@ class GitReplyEvent( } tags.add(arrayOf("alt", "a git issue reply")) - signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady) + signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady, isDraft) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesChatMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesChatMessageEvent.kt index 4f4b46eb6..c6263aee9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesChatMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/LiveActivitiesChatMessageEvent.kt @@ -72,6 +72,7 @@ class LiveActivitiesChatMessageEvent( zapRaiserAmount: Long?, geohash: String? = null, nip94attachments: List? = null, + isDraft: Boolean, onReady: (LiveActivitiesChatMessageEvent) -> Unit, ) { val content = message @@ -98,7 +99,7 @@ class LiveActivitiesChatMessageEvent( } tags.add(arrayOf("alt", ALT)) - signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) + signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady, isDraft) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/PollNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/PollNoteEvent.kt index 80d63d66a..5e2c62efd 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/PollNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/PollNoteEvent.kt @@ -80,6 +80,7 @@ class PollNoteEvent( zapRaiserAmount: Long?, geohash: String? = null, nip94attachments: List? = null, + isDraft: Boolean, onReady: (PollNoteEvent) -> Unit, ) { val tags = mutableListOf>() @@ -112,7 +113,7 @@ class PollNoteEvent( } tags.add(arrayOf("alt", ALT)) - signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady) + signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady, isDraft) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/PrivateDmEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/PrivateDmEvent.kt index adf4c1213..880b4d442 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/PrivateDmEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/PrivateDmEvent.kt @@ -124,6 +124,7 @@ class PrivateDmEvent( zapRaiserAmount: Long?, geohash: String? = null, nip94attachments: List? = null, + isDraft: Boolean, onReady: (PrivateDmEvent) -> Unit, ) { var message = msg @@ -165,7 +166,7 @@ class PrivateDmEvent( tags.add(arrayOf("alt", ALT)) signer.nip04Encrypt(message, recipientPubKey) { content -> - signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) + signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady, isDraft) } } } From f949d5624e367cdbe30469bd29eb0ae8de2b13df Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Fri, 15 Mar 2024 13:35:54 -0300 Subject: [PATCH 13/32] add draft support for public chat screen --- .../amethyst/ui/note/ChatroomMessageCompose.kt | 3 +++ .../vitorpamplona/amethyst/ui/note/NoteCompose.kt | 14 ++++++++++++++ .../amethyst/ui/screen/loggedIn/ChannelScreen.kt | 1 + .../com/vitorpamplona/amethyst/ui/theme/Shape.kt | 1 + 4 files changed, 19 insertions(+) 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 07d60234a..974f40fd3 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 @@ -455,6 +455,9 @@ private fun MessageBubbleLines( bubbleSize = bubbleSize, availableBubbleSize = availableBubbleSize, firstColumn = { + if (baseNote.isDraft()) { + DisplayDraftChat() + } IncognitoBadge(baseNote) ChatTimeAgo(baseNote) RelayBadgesHorizontal(baseNote, accountViewModel, nav = nav) 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 9da8f3202..c14fb61bf 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 @@ -115,7 +115,9 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer +import com.vitorpamplona.amethyst.ui.theme.Font12SP import com.vitorpamplona.amethyst.ui.theme.HalfDoubleVertSpacer +import com.vitorpamplona.amethyst.ui.theme.HalfEndPadding import com.vitorpamplona.amethyst.ui.theme.HalfPadding import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding import com.vitorpamplona.amethyst.ui.theme.Size25dp @@ -1128,6 +1130,18 @@ fun DisplayDraft() { ) } +@Composable +fun DisplayDraftChat() { + Text( + "Draft", + color = MaterialTheme.colorScheme.placeholderText, + modifier = HalfEndPadding, + fontWeight = FontWeight.Bold, + fontSize = Font12SP, + maxLines = 1, + ) +} + @Composable fun FirstUserInfoRow( baseNote: Note, 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 d7313e466..8609846f6 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 @@ -208,6 +208,7 @@ fun PrepareChannelViewModels( val channelScreenModel: NewPostViewModel = viewModel() channelScreenModel.accountViewModel = accountViewModel channelScreenModel.account = accountViewModel.account + channelScreenModel.originalNote = LocalCache.getNoteIfExists(baseChannel.idHex) ChannelScreen( channel = baseChannel, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index 96424ca0a..c66c5fde6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -92,6 +92,7 @@ val Size40dp = 40.dp val Size55dp = 55.dp val Size75dp = 75.dp +val HalfEndPadding = Modifier.padding(end = 5.dp) val HalfStartPadding = Modifier.padding(start = 5.dp) val StdStartPadding = Modifier.padding(start = 10.dp) val StdTopPadding = Modifier.padding(top = 10.dp) From eba0837e5277483eda03faa8d26f1fed006be1a2 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Mon, 18 Mar 2024 07:20:30 -0300 Subject: [PATCH 14/32] fix draftnotes filter --- .../main/java/com/vitorpamplona/amethyst/model/LocalCache.kt | 3 +-- 1 file changed, 1 insertion(+), 2 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 6c6f7b8a4..b85500186 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -143,9 +143,8 @@ object LocalCache { } fun draftNotes(draftTag: String): List { - return notes.values.filter { + return notes.filter { _, it -> it.event?.tags()?.filter { it.size > 1 && it[0] == "d" }?.map { - Log.d("tag", it[1]) it[1] }?.firstOrNull() == draftTag } From 6e4f1269ddc0ba795a68875dde8b7e8c4f6d0e3c Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Mon, 18 Mar 2024 07:43:19 -0300 Subject: [PATCH 15/32] add todo --- .../com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt | 1 + 1 file changed, 1 insertion(+) 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 680efad66..1ff6c7a34 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 @@ -703,6 +703,7 @@ open class NewPostViewModel() : ViewModel() { } open fun saveDraft() { + // TODO: find a way to send only the last modification so we dont get rate limited sendPost(localDraft = draftTag) } From 0a20d5484b911685fe2590e973708ba40169e19b Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Mon, 18 Mar 2024 08:39:55 -0300 Subject: [PATCH 16/32] show drafts as soon as its created --- .../main/java/com/vitorpamplona/amethyst/model/Account.kt | 7 +++++++ 1 file changed, 7 insertions(+) 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 1e59b607b..f8ae34926 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -1345,6 +1345,7 @@ class Account( DraftEvent.create(draftTag, it, signer) { draftEvent -> Client.send(draftEvent, relayList = relayList) LocalCache.justConsume(draftEvent, null) + LocalCache.justConsume(it, null) } } else { Client.send(it, relayList = relayList) @@ -1405,6 +1406,7 @@ class Account( DraftEvent.create(draftTag, it, signer) { draftEvent -> Client.send(draftEvent, relayList = relayList) LocalCache.justConsume(draftEvent, null) + LocalCache.justConsume(it, null) } } else { Client.send(it, relayList = relayList) @@ -1471,6 +1473,7 @@ class Account( DraftEvent.create(draftTag, it, signer) { draftEvent -> Client.send(draftEvent, relayList = relayList) LocalCache.justConsume(draftEvent, null) + LocalCache.justConsume(it, null) } } else { Client.send(it, relayList = relayList) @@ -1560,6 +1563,7 @@ class Account( DraftEvent.create(draftTag, it, signer) { draftEvent -> Client.send(draftEvent, relayList = relayList) LocalCache.justConsume(draftEvent, null) + LocalCache.justConsume(it, null) } } else { Client.send(it, relayList = relayList) @@ -1610,6 +1614,7 @@ class Account( DraftEvent.create(draftTag, it, signer) { draftEvent -> Client.send(draftEvent) LocalCache.justConsume(draftEvent, null) + LocalCache.justConsume(it, null) } } else { Client.send(it) @@ -1653,6 +1658,7 @@ class Account( DraftEvent.create(draftTag, it, signer) { draftEvent -> Client.send(draftEvent) LocalCache.justConsume(draftEvent, null) + LocalCache.justConsume(it, null) } } else { Client.send(it) @@ -1723,6 +1729,7 @@ class Account( DraftEvent.create(draftTag, it, signer) { draftEvent -> Client.send(draftEvent) LocalCache.justConsume(draftEvent, null) + LocalCache.justConsume(it, null) } } else { Client.send(it) From 3cc32ecd9a3d4377cc01fb7ee36c5410051ae3b6 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Mon, 18 Mar 2024 09:09:00 -0300 Subject: [PATCH 17/32] create a loadFromDraft method --- .../amethyst/ui/actions/NewPostView.kt | 3 +- .../amethyst/ui/actions/NewPostViewModel.kt | 215 +++++++++--------- 2 files changed, 114 insertions(+), 104 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 38d332163..7b80b3d4c 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 @@ -185,6 +185,7 @@ fun NewPostView( quote: Note? = null, fork: Note? = null, version: Note? = null, + draft: Note? = null, enableMessageInterface: Boolean = false, accountViewModel: AccountViewModel, nav: (String) -> Unit, @@ -201,7 +202,7 @@ fun NewPostView( LaunchedEffect(Unit) { launch(Dispatchers.IO) { - postViewModel.load(accountViewModel, baseReplyTo, quote, fork, version) + postViewModel.load(accountViewModel, baseReplyTo, quote, fork, version, draft) postViewModel.imageUploadingError.collect { error -> withContext(Dispatchers.Main) { Toast.makeText(context, error, Toast.LENGTH_SHORT).show() } 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 1ff6c7a34..ad983b7f5 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 @@ -184,120 +184,129 @@ open class NewPostViewModel() : ViewModel() { quote: Note?, fork: Note?, version: Note?, + draft: Note?, ) { this.accountViewModel = accountViewModel this.account = accountViewModel.account - originalNote = replyingTo - replyingTo?.let { replyNote -> - if (replyNote.event is BaseTextNoteEvent) { - this.eTags = (replyNote.replyTo ?: emptyList()).plus(replyNote) - } else { - this.eTags = listOf(replyNote) - } + if (draft != null) { + loadfromDraft(draft) + } else { + originalNote = replyingTo + replyingTo?.let { replyNote -> + if (replyNote.event is BaseTextNoteEvent) { + this.eTags = (replyNote.replyTo ?: emptyList()).plus(replyNote) + } else { + this.eTags = listOf(replyNote) + } - if (replyNote.event !is CommunityDefinitionEvent) { - replyNote.author?.let { replyUser -> - val currentMentions = - (replyNote.event as? TextNoteEvent)?.mentions()?.map { LocalCache.getOrCreateUser(it) } - ?: emptyList() + if (replyNote.event !is CommunityDefinitionEvent) { + replyNote.author?.let { replyUser -> + val currentMentions = + (replyNote.event as? TextNoteEvent)?.mentions()?.map { LocalCache.getOrCreateUser(it) } + ?: emptyList() - if (currentMentions.contains(replyUser)) { - this.pTags = currentMentions - } else { - this.pTags = currentMentions.plus(replyUser) + if (currentMentions.contains(replyUser)) { + this.pTags = currentMentions + } else { + this.pTags = currentMentions.plus(replyUser) + } } } } + ?: run { + eTags = null + pTags = null + } + + canAddInvoice = accountViewModel.userProfile().info?.lnAddress() != null + canAddZapRaiser = accountViewModel.userProfile().info?.lnAddress() != null + canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channelHex() == null + contentToAddUrl = null + + wantsForwardZapTo = false + wantsToMarkAsSensitive = false + wantsToAddGeoHash = false + wantsZapraiser = false + zapRaiserAmount = null + forwardZapTo = Split() + forwardZapToEditting = TextFieldValue("") + + quote?.let { + message = TextFieldValue(message.text + "\nnostr:${it.toNEvent()}") + urlPreview = findUrlInMessage() + + it.author?.let { quotedUser -> + if (quotedUser.pubkeyHex != accountViewModel.userProfile().pubkeyHex) { + if (forwardZapTo.items.none { it.key.pubkeyHex == quotedUser.pubkeyHex }) { + forwardZapTo.addItem(quotedUser) + } + if (forwardZapTo.items.none { it.key.pubkeyHex == accountViewModel.userProfile().pubkeyHex }) { + forwardZapTo.addItem(accountViewModel.userProfile()) + } + + val pos = forwardZapTo.items.indexOfFirst { it.key.pubkeyHex == quotedUser.pubkeyHex } + forwardZapTo.updatePercentage(pos, 0.9f) + } + } + } + + fork?.let { + message = TextFieldValue(version?.event?.content() ?: it.event?.content() ?: "") + urlPreview = findUrlInMessage() + + it.event?.isSensitive()?.let { + if (it) wantsToMarkAsSensitive = true + } + + it.event?.zapraiserAmount()?.let { + zapRaiserAmount = it + } + + it.event?.zapSplitSetup()?.let { + val totalWeight = it.sumOf { if (it.isLnAddress) 0.0 else it.weight } + + it.forEach { + if (!it.isLnAddress) { + forwardZapTo.addItem(LocalCache.getOrCreateUser(it.lnAddressOrPubKeyHex), (it.weight / totalWeight).toFloat()) + } + } + } + + // Only adds if it is not already set up. + if (forwardZapTo.items.isEmpty()) { + it.author?.let { forkedAuthor -> + if (forkedAuthor.pubkeyHex != accountViewModel.userProfile().pubkeyHex) { + if (forwardZapTo.items.none { it.key.pubkeyHex == forkedAuthor.pubkeyHex }) forwardZapTo.addItem(forkedAuthor) + if (forwardZapTo.items.none { it.key.pubkeyHex == accountViewModel.userProfile().pubkeyHex }) forwardZapTo.addItem(accountViewModel.userProfile()) + + val pos = forwardZapTo.items.indexOfFirst { it.key.pubkeyHex == forkedAuthor.pubkeyHex } + forwardZapTo.updatePercentage(pos, 0.8f) + } + } + } + + it.author?.let { + if (this.pTags == null) { + this.pTags = listOf(it) + } else if (this.pTags?.contains(it) != true) { + this.pTags = listOf(it) + (this.pTags ?: emptyList()) + } + } + + forkedFromNote = it + } ?: run { + forkedFromNote = null + } + + if (!forwardZapTo.items.isEmpty()) { + wantsForwardZapTo = true + } } - ?: run { - eTags = null - pTags = null - } + } - canAddInvoice = accountViewModel.userProfile().info?.lnAddress() != null - canAddZapRaiser = accountViewModel.userProfile().info?.lnAddress() != null - canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channelHex() == null - contentToAddUrl = null - - wantsForwardZapTo = false - wantsToMarkAsSensitive = false - wantsToAddGeoHash = false - wantsZapraiser = false - zapRaiserAmount = null - forwardZapTo = Split() - forwardZapToEditting = TextFieldValue("") - - quote?.let { - message = TextFieldValue(message.text + "\nnostr:${it.toNEvent()}") - urlPreview = findUrlInMessage() - - it.author?.let { quotedUser -> - if (quotedUser.pubkeyHex != accountViewModel.userProfile().pubkeyHex) { - if (forwardZapTo.items.none { it.key.pubkeyHex == quotedUser.pubkeyHex }) { - forwardZapTo.addItem(quotedUser) - } - if (forwardZapTo.items.none { it.key.pubkeyHex == accountViewModel.userProfile().pubkeyHex }) { - forwardZapTo.addItem(accountViewModel.userProfile()) - } - - val pos = forwardZapTo.items.indexOfFirst { it.key.pubkeyHex == quotedUser.pubkeyHex } - forwardZapTo.updatePercentage(pos, 0.9f) - } - } - } - - fork?.let { - message = TextFieldValue(version?.event?.content() ?: it.event?.content() ?: "") - urlPreview = findUrlInMessage() - - it.event?.isSensitive()?.let { - if (it) wantsToMarkAsSensitive = true - } - - it.event?.zapraiserAmount()?.let { - zapRaiserAmount = it - } - - it.event?.zapSplitSetup()?.let { - val totalWeight = it.sumOf { if (it.isLnAddress) 0.0 else it.weight } - - it.forEach { - if (!it.isLnAddress) { - forwardZapTo.addItem(LocalCache.getOrCreateUser(it.lnAddressOrPubKeyHex), (it.weight / totalWeight).toFloat()) - } - } - } - - // Only adds if it is not already set up. - if (forwardZapTo.items.isEmpty()) { - it.author?.let { forkedAuthor -> - if (forkedAuthor.pubkeyHex != accountViewModel.userProfile().pubkeyHex) { - if (forwardZapTo.items.none { it.key.pubkeyHex == forkedAuthor.pubkeyHex }) forwardZapTo.addItem(forkedAuthor) - if (forwardZapTo.items.none { it.key.pubkeyHex == accountViewModel.userProfile().pubkeyHex }) forwardZapTo.addItem(accountViewModel.userProfile()) - - val pos = forwardZapTo.items.indexOfFirst { it.key.pubkeyHex == forkedAuthor.pubkeyHex } - forwardZapTo.updatePercentage(pos, 0.8f) - } - } - } - - it.author?.let { - if (this.pTags == null) { - this.pTags = listOf(it) - } else if (this.pTags?.contains(it) != true) { - this.pTags = listOf(it) + (this.pTags ?: emptyList()) - } - } - - forkedFromNote = it - } ?: run { - forkedFromNote = null - } - - if (!forwardZapTo.items.isEmpty()) { - wantsForwardZapTo = true - } + private fun loadfromDraft(draft: Note?) { + // TODO: finish the loadfromDraft method } fun sendPost( From 1c249eed2020c2e8bc491a697a26f91e752ae1e1 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Mon, 18 Mar 2024 10:56:36 -0300 Subject: [PATCH 18/32] load post from draft --- .../amethyst/ui/actions/NewPostViewModel.kt | 66 ++++++++++++++++++- .../ui/components/ZapRaiserRequest.kt | 1 + .../amethyst/ui/note/elements/DropDownMenu.kt | 24 +++++++ 3 files changed, 88 insertions(+), 3 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 ad983b7f5..de7299964 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 @@ -190,7 +190,7 @@ open class NewPostViewModel() : ViewModel() { this.account = accountViewModel.account if (draft != null) { - loadfromDraft(draft) + loadfromDraft(draft, accountViewModel) } else { originalNote = replyingTo replyingTo?.let { replyNote -> @@ -305,8 +305,68 @@ open class NewPostViewModel() : ViewModel() { } } - private fun loadfromDraft(draft: Note?) { - // TODO: finish the loadfromDraft method + private fun loadfromDraft( + draft: Note, + accountViewModel: AccountViewModel, + ) { + Log.d("draft", draft.event?.toJson().toString()) + + canAddInvoice = accountViewModel.userProfile().info?.lnAddress() != null + canAddZapRaiser = accountViewModel.userProfile().info?.lnAddress() != null + contentToAddUrl = null + + val localfowardZapTo = draft.event?.tags()?.filter { it.size > 1 && it[0] == "zap" } ?: listOf() + forwardZapTo = Split() + localfowardZapTo.forEach { + val user = LocalCache.getOrCreateUser(it[1]) + val value = it.last().toFloatOrNull() ?: 0f + forwardZapTo.addItem(user, value) + } + forwardZapToEditting = TextFieldValue("") + wantsForwardZapTo = localfowardZapTo.isNotEmpty() + + wantsToMarkAsSensitive = draft.event?.tags()?.any { it.size > 1 && it[0] == "content-warning" } ?: false + wantsToAddGeoHash = draft.event?.tags()?.any { it.size > 1 && it[0] == "g" } ?: false + val zapraiser = draft.event?.tags()?.filter { it.size > 1 && it[0] == "zapraiser" } ?: listOf() + wantsZapraiser = zapraiser.isNotEmpty() + zapRaiserAmount = null + if (wantsZapraiser) { + zapRaiserAmount = zapraiser.first()[1].toLongOrNull() ?: 0 + } + + eTags = + draft.event?.tags()?.filter { it.size > 1 && it[0] == "e" && it.getOrNull(3) != "fork" }?.map { + val forked = it[3] == "fork" + val note = LocalCache.getOrCreateNote(it[1]) + if (forked) { + forkedFromNote = note + } + note + } + + pTags = + draft.event?.tags()?.filter { it.size > 1 && it[0] == "p" }?.map { + LocalCache.getOrCreateUser(it[1]) + } + + draft.event?.tags()?.filter { it.size > 1 && it[0] == "e" && it.getOrNull(3) == "fork" }?.forEach { + val note = LocalCache.getOrCreateNote(it[1]) + forkedFromNote = note + } + + originalNote = + draft.event?.tags()?.filter { it.size > 1 && it[0] == "e" && it.getOrNull(3) == "root" }?.map { + LocalCache.getOrCreateNote(it[1]) + }?.firstOrNull() + + canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channelHex() == null + + if (forwardZapTo.items.isNotEmpty()) { + wantsForwardZapTo = true + } + + message = TextFieldValue(draft.event?.content() ?: "") + urlPreview = findUrlInMessage() } fun sendPost( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZapRaiserRequest.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZapRaiserRequest.kt index 8d59c824c..2e8bef95a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZapRaiserRequest.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZapRaiserRequest.kt @@ -97,6 +97,7 @@ fun ZapRaiserRequest( } else { newPostViewModel.zapRaiserAmount = it.toLongOrNull() } + newPostViewModel.saveDraft() } }, placeholder = { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt index 27d37e018..b9672a075 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt @@ -45,6 +45,7 @@ import androidx.core.content.ContextCompat import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.actions.EditPostView +import com.vitorpamplona.amethyst.ui.actions.NewPostView import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon import com.vitorpamplona.amethyst.ui.note.externalLinkForNote @@ -122,6 +123,11 @@ fun NoteDropDownMenu( mutableStateOf(false) } + val wantsToEditDraft = + remember { + mutableStateOf(false) + } + if (wantsToEditPost.value) { // avoids changing while drafting a note and a new event shows up. val versionLookingAt = @@ -141,6 +147,18 @@ fun NoteDropDownMenu( ) } + if (wantsToEditDraft.value) { + NewPostView( + onClose = { + popupExpanded.value = false + wantsToEditDraft.value = false + }, + accountViewModel = accountViewModel, + draft = note, + nav = nav, + ) + } + DropdownMenu( expanded = popupExpanded.value, onDismissRequest = onDismiss, @@ -219,6 +237,12 @@ fun NoteDropDownMenu( }, ) HorizontalDivider(thickness = DividerThickness) + DropdownMenuItem( + text = { Text(stringResource(R.string.edit_draft)) }, + onClick = { + wantsToEditDraft.value = true + }, + ) if (note.event is TextNoteEvent) { if (state.isLoggedUser) { DropdownMenuItem( From 204eaa4606787c033881659446fd753f5110dc2a Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Mon, 18 Mar 2024 11:16:13 -0300 Subject: [PATCH 19/32] fix crash when loading draft from channels or lives --- .../amethyst/ui/actions/NewPostViewModel.kt | 12 ++++-------- 1 file changed, 4 insertions(+), 8 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 de7299964..38fc5862a 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 @@ -335,12 +335,8 @@ open class NewPostViewModel() : ViewModel() { } eTags = - draft.event?.tags()?.filter { it.size > 1 && it[0] == "e" && it.getOrNull(3) != "fork" }?.map { - val forked = it[3] == "fork" - val note = LocalCache.getOrCreateNote(it[1]) - if (forked) { - forkedFromNote = note - } + draft.event?.tags()?.filter { it.size > 1 && it[0] == "e" && it.getOrNull(3) != "fork" }?.mapNotNull { + val note = LocalCache.checkGetOrCreateNote(it[1]) note } @@ -350,13 +346,13 @@ open class NewPostViewModel() : ViewModel() { } draft.event?.tags()?.filter { it.size > 1 && it[0] == "e" && it.getOrNull(3) == "fork" }?.forEach { - val note = LocalCache.getOrCreateNote(it[1]) + val note = LocalCache.checkGetOrCreateNote(it[1]) forkedFromNote = note } originalNote = draft.event?.tags()?.filter { it.size > 1 && it[0] == "e" && it.getOrNull(3) == "root" }?.map { - LocalCache.getOrCreateNote(it[1]) + LocalCache.checkGetOrCreateNote(it[1]) }?.firstOrNull() canUsePoll = originalNote?.event !is PrivateDmEvent && originalNote?.channelHex() == null From f7ab925b1d3c004d0c8e424ff0f9407cea7525d4 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Mon, 18 Mar 2024 14:21:58 -0300 Subject: [PATCH 20/32] fix delete on close --- .../vitorpamplona/amethyst/model/Account.kt | 7 +++++++ .../amethyst/model/LocalCache.kt | 19 +++++++++++++------ .../service/NostrAccountDataSource.kt | 9 +++++++++ .../amethyst/ui/actions/NewPostViewModel.kt | 6 ++++-- 4 files changed, 33 insertions(+), 8 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 f8ae34926..3ded4fd14 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -1346,6 +1346,7 @@ class Account( Client.send(draftEvent, relayList = relayList) LocalCache.justConsume(draftEvent, null) LocalCache.justConsume(it, null) + LocalCache.addDraft(draftTag, it.id()) } } else { Client.send(it, relayList = relayList) @@ -1407,6 +1408,7 @@ class Account( Client.send(draftEvent, relayList = relayList) LocalCache.justConsume(draftEvent, null) LocalCache.justConsume(it, null) + LocalCache.addDraft(draftTag, it.id()) } } else { Client.send(it, relayList = relayList) @@ -1474,6 +1476,7 @@ class Account( Client.send(draftEvent, relayList = relayList) LocalCache.justConsume(draftEvent, null) LocalCache.justConsume(it, null) + LocalCache.addDraft(draftTag, it.id()) } } else { Client.send(it, relayList = relayList) @@ -1564,6 +1567,7 @@ class Account( Client.send(draftEvent, relayList = relayList) LocalCache.justConsume(draftEvent, null) LocalCache.justConsume(it, null) + LocalCache.addDraft(draftTag, it.id()) } } else { Client.send(it, relayList = relayList) @@ -1615,6 +1619,7 @@ class Account( Client.send(draftEvent) LocalCache.justConsume(draftEvent, null) LocalCache.justConsume(it, null) + LocalCache.addDraft(draftTag, it.id()) } } else { Client.send(it) @@ -1659,6 +1664,7 @@ class Account( Client.send(draftEvent) LocalCache.justConsume(draftEvent, null) LocalCache.justConsume(it, null) + LocalCache.addDraft(draftTag, it.id()) } } else { Client.send(it) @@ -1730,6 +1736,7 @@ class Account( Client.send(draftEvent) LocalCache.justConsume(draftEvent, null) LocalCache.justConsume(it, null) + LocalCache.addDraft(draftTag, it.id()) } } else { Client.send(it) 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 b85500186..ccde9523a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -129,7 +129,7 @@ object LocalCache { val users = LargeCache() val notes = LargeCache() val addressables = LargeCache() - + val drafts = ConcurrentHashMap>() val channels = ConcurrentHashMap() val awaitingPaymentRequests = ConcurrentHashMap Unit>>(10) @@ -143,11 +143,18 @@ object LocalCache { } fun draftNotes(draftTag: String): List { - return notes.filter { _, it -> - it.event?.tags()?.filter { it.size > 1 && it[0] == "d" }?.map { - it[1] - }?.firstOrNull() == draftTag - } + return drafts[draftTag]?.mapNotNull { + checkGetOrCreateNote(it) + } ?: listOf() + } + + fun addDraft( + key: String, + value: String, + ) { + val data = drafts[key] ?: mutableListOf() + data.add(value) + drafts[key] = data } fun getOrCreateUser(key: HexKey): User { 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 f0067ed74..9afedce15 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt @@ -276,11 +276,20 @@ object NostrAccountDataSource : NostrDataSource("AccountData") { when (event) { is DraftEvent -> { // Avoid decrypting over and over again if the event already exist. + val note = LocalCache.getNoteIfExists(event.id) if (note != null && relay.brief in note.relays) return event.plainContent(account.signer) { + val tag = + event.tags().filter { it.size > 1 && it[0] == "d" }.map { + it[1] + }.firstOrNull() + LocalCache.justConsume(it, relay) + tag?.let { lTag -> + LocalCache.addDraft(lTag, it.id()) + } } } 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 38fc5862a..8c5a03ae5 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 @@ -83,7 +83,7 @@ enum class UserSuggestionAnchor { @Stable open class NewPostViewModel() : ViewModel() { - var draftTag: String = UUID.randomUUID().toString() + private var draftTag: String = UUID.randomUUID().toString() var accountViewModel: AccountViewModel? = null var account: Account? = null var requiresNIP24: Boolean = false @@ -309,7 +309,9 @@ open class NewPostViewModel() : ViewModel() { draft: Note, accountViewModel: AccountViewModel, ) { - Log.d("draft", draft.event?.toJson().toString()) + draftTag = LocalCache.drafts.filter { + it.value.contains(draft.idHex) + }.keys.firstOrNull() ?: draftTag canAddInvoice = accountViewModel.userProfile().info?.lnAddress() != null canAddZapRaiser = accountViewModel.userProfile().info?.lnAddress() != null From 84faa7557e056f0194c7b2a2c475fd7ea25b10da Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Mon, 18 Mar 2024 14:37:51 -0300 Subject: [PATCH 21/32] add support for polls --- .../amethyst/ui/actions/NewPostViewModel.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) 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 8c5a03ae5..08fde7898 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 @@ -309,6 +309,8 @@ open class NewPostViewModel() : ViewModel() { draft: Note, accountViewModel: AccountViewModel, ) { + Log.d("draft", draft.event!!.toJson()) + draftTag = LocalCache.drafts.filter { it.value.contains(draft.idHex) }.keys.firstOrNull() ?: draftTag @@ -363,6 +365,22 @@ open class NewPostViewModel() : ViewModel() { wantsForwardZapTo = true } + val polls = draft.event?.tags()?.filter { it.size > 1 && it[0] == "poll_option" } ?: emptyList() + wantsPoll = polls.isNotEmpty() + + polls.forEach { + pollOptions[it[1].toInt()] = it[2] + } + + val minMax = draft.event?.tags()?.filter { it.size > 1 && (it[0] == "value_minimum" || it[0] == "value_maximum") } ?: listOf() + minMax.forEach { + if (it[0] == "value_maximum") { + valueMaximum = it[1].toInt() + } else if (it[0] == "value_minimum") { + valueMinimum = it[1].toInt() + } + } + message = TextFieldValue(draft.event?.content() ?: "") urlPreview = findUrlInMessage() } From 8b3e3e7af85b9e3143f8f6f6c01474ec354ee860 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Mon, 18 Mar 2024 14:51:50 -0300 Subject: [PATCH 22/32] support Classifieds --- .../amethyst/ui/actions/NewPostViewModel.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 08fde7898..465139241 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 @@ -381,6 +381,16 @@ open class NewPostViewModel() : ViewModel() { } } + wantsProduct = draft.event?.kind() == 30402 + + title = TextFieldValue(draft.event?.tags()?.filter { it.size > 1 && it[0] == "title" }?.map { it[1] }?.firstOrNull() ?: "") + price = TextFieldValue(draft.event?.tags()?.filter { it.size > 1 && it[0] == "price" }?.map { it[1] }?.firstOrNull() ?: "") + category = TextFieldValue(draft.event?.tags()?.filter { it.size > 1 && it[0] == "t" }?.map { it[1] }?.firstOrNull() ?: "") + locationText = TextFieldValue(draft.event?.tags()?.filter { it.size > 1 && it[0] == "location" }?.map { it[1] }?.firstOrNull() ?: "") + condition = ClassifiedsEvent.CONDITION.entries.firstOrNull { + it.value == draft.event?.tags()?.filter { it.size > 1 && it[0] == "condition" }?.map { it[1] }?.firstOrNull() + } ?: ClassifiedsEvent.CONDITION.USED_LIKE_NEW + message = TextFieldValue(draft.event?.content() ?: "") urlPreview = findUrlInMessage() } From f6e5af3e98247a403754e1855d394bb3d9792830 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Mon, 18 Mar 2024 16:18:12 -0300 Subject: [PATCH 23/32] support for dms, streams and communities --- .../com/vitorpamplona/amethyst/model/Account.kt | 13 ++++++++++++- .../amethyst/ui/actions/NewPostViewModel.kt | 7 ++++--- .../amethyst/ui/screen/loggedIn/ChatroomScreen.kt | 10 +++++++++- .../quartz/events/ChatMessageEvent.kt | 3 ++- .../vitorpamplona/quartz/events/NIP24Factory.kt | 15 +++++++++++++-- 5 files changed, 40 insertions(+), 8 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 3ded4fd14..a5daff826 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -1756,6 +1756,7 @@ class Account( zapRaiserAmount: Long? = null, geohash: String? = null, nip94attachments: List? = null, + draftTag: String? = null, ) { if (!isWriteable()) return @@ -1773,9 +1774,19 @@ class Account( zapRaiserAmount = zapRaiserAmount, geohash = geohash, nip94attachments = nip94attachments, + draftTag = draftTag, signer = signer, ) { - broadcastPrivately(it) + if (draftTag != null) { + DraftEvent.create(draftTag, it.msg, signer) { draftEvent -> + Client.send(draftEvent) + LocalCache.justConsume(draftEvent, null) + LocalCache.justConsume(it.msg, null) + LocalCache.addDraft(draftTag, it.msg.id()) + } + } else { + broadcastPrivately(it) + } } } 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 465139241..f14fa3be4 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 @@ -339,7 +339,7 @@ open class NewPostViewModel() : ViewModel() { } eTags = - draft.event?.tags()?.filter { it.size > 1 && it[0] == "e" && it.getOrNull(3) != "fork" }?.mapNotNull { + draft.event?.tags()?.filter { it.size > 1 && (it[0] == "e" || it[0] == "a") && it.getOrNull(3) != "fork" }?.mapNotNull { val note = LocalCache.checkGetOrCreateNote(it[1]) note } @@ -349,13 +349,13 @@ open class NewPostViewModel() : ViewModel() { LocalCache.getOrCreateUser(it[1]) } - draft.event?.tags()?.filter { it.size > 1 && it[0] == "e" && it.getOrNull(3) == "fork" }?.forEach { + draft.event?.tags()?.filter { it.size > 1 && (it[0] == "e" || it[0] == "a") && it.getOrNull(3) == "fork" }?.forEach { val note = LocalCache.checkGetOrCreateNote(it[1]) forkedFromNote = note } originalNote = - draft.event?.tags()?.filter { it.size > 1 && it[0] == "e" && it.getOrNull(3) == "root" }?.map { + draft.event?.tags()?.filter { it.size > 1 && (it[0] == "e" || it[0] == "a") && it.getOrNull(3) == "root" }?.map { LocalCache.checkGetOrCreateNote(it[1]) }?.firstOrNull() @@ -529,6 +529,7 @@ open class NewPostViewModel() : ViewModel() { zapRaiserAmount = localZapRaiserAmount, geohash = geoHash, nip94attachments = usedAttachments, + draftTag = localDraft, ) } else { account?.sendPrivateMessage( 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 76f3c98b0..8367b2191 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 @@ -116,6 +116,8 @@ import com.vitorpamplona.amethyst.ui.theme.Size34dp import com.vitorpamplona.amethyst.ui.theme.StdPadding import com.vitorpamplona.amethyst.ui.theme.ZeroPadding import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.encoders.Hex +import com.vitorpamplona.quartz.encoders.toNpub import com.vitorpamplona.quartz.events.ChatMessageEvent import com.vitorpamplona.quartz.events.ChatroomKey import com.vitorpamplona.quartz.events.findURLs @@ -230,6 +232,9 @@ fun PrepareChatroomViewModels( if (newPostModel.requiresNIP24) { newPostModel.nip24 = true } + room.users.forEach { + newPostModel.toUsers = TextFieldValue(newPostModel.toUsers.text + " @${Hex.decode(it).toNpub()}") + } LaunchedEffect(key1 = newPostModel) { launch(Dispatchers.IO) { @@ -315,7 +320,10 @@ fun ChatroomScreen( accountViewModel = accountViewModel, nav = nav, routeForLastRead = "Room/${room.hashCode()}", - onWantsToReply = { replyTo.value = it }, + onWantsToReply = { + replyTo.value = it + newPostModel.originalNote = it + }, ) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChatMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChatMessageEvent.kt index 1c2566d7b..805f5884f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChatMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChatMessageEvent.kt @@ -82,6 +82,7 @@ class ChatMessageEvent( signer: NostrSigner, createdAt: Long = TimeUtils.now(), nip94attachments: List? = null, + isDraft: Boolean, onReady: (ChatMessageEvent) -> Unit, ) { val tags = mutableListOf>() @@ -106,7 +107,7 @@ class ChatMessageEvent( } // tags.add(arrayOf("alt", alt)) - signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady) + signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady, isDraft) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP24Factory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP24Factory.kt index 4c623152f..6e69a305d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP24Factory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP24Factory.kt @@ -77,6 +77,7 @@ class NIP24Factory { zapRaiserAmount: Long? = null, geohash: String? = null, nip94attachments: List? = null, + draftTag: String? = null, onReady: (Result) -> Unit, ) { val senderPublicKey = signer.pubKey @@ -92,15 +93,25 @@ class NIP24Factory { markAsSensitive = markAsSensitive, zapRaiserAmount = zapRaiserAmount, geohash = geohash, + isDraft = draftTag != null, nip94attachments = nip94attachments, ) { senderMessage -> - createWraps(senderMessage, to.plus(senderPublicKey).toSet(), signer) { wraps -> + if (draftTag != null) { onReady( Result( msg = senderMessage, - wraps = wraps, + wraps = listOf(), ), ) + } else { + createWraps(senderMessage, to.plus(senderPublicKey).toSet(), signer) { wraps -> + onReady( + Result( + msg = senderMessage, + wraps = wraps, + ), + ) + } } } } From 940fa2ee8d110709f884755a835d9c6d09fc3574 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Mon, 18 Mar 2024 17:47:15 -0300 Subject: [PATCH 24/32] add debouncer --- .../amethyst/ui/actions/NewPostView.kt | 15 +++++++++++- .../amethyst/ui/actions/NewPostViewModel.kt | 24 ++++++++++++------- .../ui/components/ZapRaiserRequest.kt | 7 +++++- .../ui/screen/loggedIn/ChannelScreen.kt | 24 ++++++++++++++++++- .../ui/screen/loggedIn/ChatroomScreen.kt | 14 +++++++++++ 5 files changed, 72 insertions(+), 12 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 7b80b3d4c..649532377 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 @@ -171,13 +171,18 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.lang.Math.round -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, FlowPreview::class) @Composable fun NewPostView( onClose: () -> Unit, @@ -201,6 +206,14 @@ fun NewPostView( var relayList = remember { accountViewModel.account.activeWriteRelays().toImmutableList() } LaunchedEffect(Unit) { + launch(Dispatchers.IO) { + postViewModel.draftTextChanges + .receiveAsFlow() + .debounce(1000) + .collectLatest { + postViewModel.sendPost(relayList = relayList, localDraft = postViewModel.draftTag) + } + } launch(Dispatchers.IO) { postViewModel.load(accountViewModel, baseReplyTo, quote, fork, version, draft) 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 f14fa3be4..a177a0b9c 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 @@ -69,6 +69,7 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.mapLatest @@ -83,7 +84,7 @@ enum class UserSuggestionAnchor { @Stable open class NewPostViewModel() : ViewModel() { - private var draftTag: String = UUID.randomUUID().toString() + var draftTag: String = UUID.randomUUID().toString() var accountViewModel: AccountViewModel? = null var account: Account? = null var requiresNIP24: Boolean = false @@ -166,6 +167,8 @@ open class NewPostViewModel() : ViewModel() { // NIP24 Wrapped DMs / Group messages var nip24 by mutableStateOf(false) + val draftTextChanges = Channel(Channel.CONFLATED) + fun lnAddress(): String? { return account?.userProfile()?.info?.lnAddress() } @@ -190,7 +193,7 @@ open class NewPostViewModel() : ViewModel() { this.account = accountViewModel.account if (draft != null) { - loadfromDraft(draft, accountViewModel) + loadFromDraft(draft, accountViewModel) } else { originalNote = replyingTo replyingTo?.let { replyNote -> @@ -305,7 +308,7 @@ open class NewPostViewModel() : ViewModel() { } } - private fun loadfromDraft( + private fun loadFromDraft( draft: Note, accountViewModel: AccountViewModel, ) { @@ -798,9 +801,8 @@ open class NewPostViewModel() : ViewModel() { pTags = pTags?.filter { it != userToRemove } } - open fun saveDraft() { - // TODO: find a way to send only the last modification so we dont get rate limited - sendPost(localDraft = draftTag) + open suspend fun saveDraft() { + draftTextChanges.send("") } open fun updateMessage(it: TextFieldValue) { @@ -1010,7 +1012,9 @@ open class NewPostViewModel() : ViewModel() { message = message.insertUrlAtCursor(imageUrl) urlPreview = findUrlInMessage() - saveDraft() + viewModelScope.launch(Dispatchers.IO) { + saveDraft() + } } }, onError = { @@ -1055,7 +1059,9 @@ open class NewPostViewModel() : ViewModel() { } urlPreview = findUrlInMessage() - saveDraft() + viewModelScope.launch(Dispatchers.IO) { + saveDraft() + } } }, onError = { @@ -1076,7 +1082,7 @@ open class NewPostViewModel() : ViewModel() { locUtil?.let { location = it.locationStateFlow.mapLatest { it.toGeoHash(GeohashPrecision.KM_5_X_5.digits).toString() } - saveDraft() + viewModelScope.launch(Dispatchers.IO) { saveDraft() } } viewModelScope.launch(Dispatchers.IO) { locUtil?.start() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZapRaiserRequest.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZapRaiserRequest.kt index 2e8bef95a..ce9bb577e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZapRaiserRequest.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZapRaiserRequest.kt @@ -40,11 +40,14 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.placeholderText +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch @Composable fun ZapRaiserRequest( @@ -97,7 +100,9 @@ fun ZapRaiserRequest( } else { newPostViewModel.zapRaiserAmount = it.toLongOrNull() } - newPostViewModel.saveDraft() + newPostViewModel.viewModelScope.launch(Dispatchers.IO) { + newPostViewModel.saveDraft() + } } }, placeholder = { 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 5f764c63b..f90a8a85b 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 @@ -165,6 +165,10 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.text.DateFormat @@ -189,6 +193,7 @@ fun ChannelScreen( } } +@OptIn(FlowPreview::class) @Composable fun PrepareChannelViewModels( baseChannel: Channel, @@ -206,6 +211,18 @@ fun PrepareChannelViewModels( ) val channelScreenModel: NewPostViewModel = viewModel() + + LaunchedEffect(Unit) { + launch(Dispatchers.IO) { + channelScreenModel.draftTextChanges + .receiveAsFlow() + .debounce(1000) + .collectLatest { + channelScreenModel.sendPost(localDraft = channelScreenModel.draftTag) + } + } + } + channelScreenModel.accountViewModel = accountViewModel channelScreenModel.account = accountViewModel.account channelScreenModel.originalNote = LocalCache.getNoteIfExists(baseChannel.idHex) @@ -680,7 +697,12 @@ fun ShowVideoStreaming( Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, - modifier = remember { Modifier.fillMaxWidth().heightIn(min = 50.dp, max = 300.dp) }, + modifier = + remember { + Modifier + .fillMaxWidth() + .heightIn(min = 50.dp, max = 300.dp) + }, ) { val zoomableUrlVideo = remember(streamingInfo) { 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 8367b2191..da63ece2d 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 @@ -124,6 +124,10 @@ import com.vitorpamplona.quartz.events.findURLs import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -208,6 +212,7 @@ fun LoadRoomByAuthor( content(room) } +@OptIn(FlowPreview::class) @Composable fun PrepareChatroomViewModels( room: ChatroomKey, @@ -237,6 +242,15 @@ fun PrepareChatroomViewModels( } LaunchedEffect(key1 = newPostModel) { + launch(Dispatchers.IO) { + newPostModel.draftTextChanges + .receiveAsFlow() + .debounce(1000) + .collectLatest { + newPostModel.sendPost(localDraft = newPostModel.draftTag) + } + } + launch(Dispatchers.IO) { val hasNIP24 = accountViewModel.userProfile().privateChatrooms[room]?.roomMessages?.any { From 5910ef199f50ddb012194ad745a9f0208fff7a2c Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Wed, 20 Mar 2024 07:22:55 -0300 Subject: [PATCH 25/32] fix build after merge --- .../amethyst/ui/screen/loggedIn/AccountViewModel.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 7061349b0..a7abcbb9a 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 @@ -1304,11 +1304,10 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View } } - suspend fun deleteDraft(draftTag: String) { val notes = LocalCache.draftNotes(draftTag) account.delete(notes) - } + } val bechLinkCache = CachedLoadedBechLink(this) From bc180ae210e02dc6297517016366c4a7fe3b29f2 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Wed, 20 Mar 2024 09:31:31 -0300 Subject: [PATCH 26/32] fix draft delete not working --- .../com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt | 1 + 1 file changed, 1 insertion(+) 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 9afedce15..5507975b7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt @@ -280,6 +280,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") { val note = LocalCache.getNoteIfExists(event.id) if (note != null && relay.brief in note.relays) return + LocalCache.justConsume(event, relay) event.plainContent(account.signer) { val tag = event.tags().filter { it.size > 1 && it[0] == "d" }.map { From 220ce75f199275b73e80c61edb037fa152ade486 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Wed, 20 Mar 2024 10:25:41 -0300 Subject: [PATCH 27/32] add validations to draft notes --- .../vitorpamplona/amethyst/model/Account.kt | 24 ++++++++++--- .../amethyst/model/LocalCache.kt | 8 +++++ .../amethyst/ui/note/ReactionsRow.kt | 34 +++++++++++++++++-- .../amethyst/ui/note/ZapCustomDialog.kt | 2 +- .../amethyst/ui/note/elements/DropDownMenu.kt | 16 +++++---- .../ui/screen/loggedIn/AccountViewModel.kt | 8 +++++ app/src/main/res/values/strings.xml | 5 +++ 7 files changed, 83 insertions(+), 14 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 fc84e4455..619e70347 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -846,7 +846,14 @@ class Account( } suspend fun delete(note: Note) { - return delete(listOf(note)) + if (note.isDraft()) { + note.event?.let { + val drafts = LocalCache.getDrafts(it.id()) + return delete(drafts) + } + } else { + return delete(listOf(note)) + } } suspend fun delete(notes: List) { @@ -898,10 +905,17 @@ class Account( fun broadcast(note: Note) { note.event?.let { - if (it is WrappedEvent && it.host != null) { - it.host?.let { hostEvent -> Client.send(hostEvent) } + if (note.isDraft()) { + val drafts = LocalCache.getDrafts(it.id()) + drafts.forEach { draftNote -> + broadcast(draftNote) + } } else { - Client.send(it) + if (it is WrappedEvent && it.host != null) { + it.host?.let { hostEvent -> Client.send(hostEvent) } + } else { + Client.send(it) + } } } } @@ -930,6 +944,7 @@ class Account( fun timestamp(note: Note) { if (!isWriteable()) return + if (note.isDraft()) return val id = note.event?.id() ?: note.idHex @@ -1942,6 +1957,7 @@ class Account( isPrivate: Boolean, ) { if (!isWriteable()) return + if (note.isDraft()) return if (note is AddressableNote) { BookmarkListEvent.addReplaceable( 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 64f2e5114..3e6a8309d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -148,6 +148,14 @@ object LocalCache { } ?: listOf() } + fun getDrafts(eventId: String): List { + return drafts.filter { + it.value.any { it == eventId } + }.keys.mapNotNull { + checkGetOrCreateNote(it) + } + } + fun addDraft( key: String, value: String, 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 8fde12944..c70684c38 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 @@ -307,7 +307,12 @@ fun RenderZapRaiser( } LinearProgressIndicator( - modifier = remember(details) { Modifier.fillMaxWidth().height(if (details) 24.dp else 4.dp) }, + modifier = + remember(details) { + Modifier + .fillMaxWidth() + .height(if (details) 24.dp else 4.dp) + }, color = color, progress = { zapraiserStatus.progress }, ) @@ -587,6 +592,13 @@ fun ReplyReaction( IconButton( modifier = iconSizeModifier, onClick = { + if (baseNote.isDraft()) { + accountViewModel.toast( + R.string.draft_note, + R.string.it_s_not_possible_to_reply_to_a_draft_note, + ) + return@IconButton + } if (accountViewModel.isWriteable()) { onPress() } else { @@ -774,7 +786,8 @@ fun LikeReaction( Box( contentAlignment = Center, modifier = - Modifier.size(iconSize) + Modifier + .size(iconSize) .combinedClickable( role = Role.Button, interactionSource = remember { MutableInteractionSource() }, @@ -782,6 +795,7 @@ fun LikeReaction( onClick = { likeClick( accountViewModel, + baseNote, onMultipleChoices = { wantsToReact = true }, onWantsToSignReaction = { accountViewModel.reactToOrDelete(baseNote) }, ) @@ -884,9 +898,17 @@ fun ObserveLikeText( private fun likeClick( accountViewModel: AccountViewModel, + baseNote: Note, onMultipleChoices: () -> Unit, onWantsToSignReaction: () -> Unit, ) { + if (baseNote.isDraft()) { + accountViewModel.toast( + R.string.draft_note, + R.string.it_s_not_possible_to_react_to_a_draft_note, + ) + return + } if (accountViewModel.account.reactionChoices.isEmpty()) { accountViewModel.toast( R.string.no_reactions_setup, @@ -1080,6 +1102,14 @@ fun zapClick( onError: (String, String) -> Unit, onPayViaIntent: (ImmutableList) -> Unit, ) { + if (baseNote.isDraft()) { + accountViewModel.toast( + R.string.draft_note, + R.string.it_s_not_possible_to_zap_to_a_draft_note, + ) + return + } + if (accountViewModel.account.zapAmountChoices.isEmpty()) { accountViewModel.toast( context.getString(R.string.error_dialog_zap_error), 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 ae411dee6..61dd389ad 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 @@ -179,7 +179,7 @@ fun ZapCustomDialog( ) ZapButton( - isActive = postViewModel.canSend(), + isActive = postViewModel.canSend() && !baseNote.isDraft(), ) { accountViewModel.zap( baseNote, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt index b9672a075..70cc123c2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt @@ -237,13 +237,15 @@ fun NoteDropDownMenu( }, ) HorizontalDivider(thickness = DividerThickness) - DropdownMenuItem( - text = { Text(stringResource(R.string.edit_draft)) }, - onClick = { - wantsToEditDraft.value = true - }, - ) - if (note.event is TextNoteEvent) { + if (!note.isDraft()) { + DropdownMenuItem( + text = { Text(stringResource(R.string.edit_draft)) }, + onClick = { + wantsToEditDraft.value = true + }, + ) + } + if (note.event is TextNoteEvent && !note.isDraft()) { if (state.isLoggedUser) { DropdownMenuItem( text = { Text(stringResource(R.string.edit_post)) }, 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 a7abcbb9a..d4d03478d 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 @@ -1209,6 +1209,14 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View baseNote: Note, onMore: () -> Unit, ) { + if (baseNote.isDraft()) { + toast( + R.string.draft_note, + R.string.it_s_not_possible_to_quote_to_a_draft_note, + ) + return + } + if (isWriteable()) { if (hasBoosted(baseNote)) { deleteBoostsTo(baseNote) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9e117b505..fde9df12d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -821,4 +821,9 @@ Play username as audio Scan QR code Navigate to the third-party wallet provider Alby + It\'s not possible to reply a draft note + It\'s not possible to quote a draft note + It\'s not possible to react a draft note + It\'s not possible to zap a draft note + Draft Note From 090b643f43dee7e6ece98d6ad7177e13c49868bc Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Wed, 20 Mar 2024 14:45:45 -0300 Subject: [PATCH 28/32] add edit draft in the dropdown menu and the long press popup --- app/src/main/java/com/vitorpamplona/amethyst/model/Drafts.kt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/model/Drafts.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Drafts.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Drafts.kt new file mode 100644 index 000000000..f01d30a16 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Drafts.kt @@ -0,0 +1,2 @@ +package com.vitorpamplona.amethyst.model + From ea33cc77edba4b352cca21f572a54d9ab2c7250d Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Wed, 20 Mar 2024 14:50:53 -0300 Subject: [PATCH 29/32] add edit draft in the dropdown menu and the long press popup --- .../vitorpamplona/amethyst/model/Account.kt | 16 +++--- .../vitorpamplona/amethyst/model/Drafts.kt | 21 ++++++++ .../amethyst/model/LocalCache.kt | 23 ++++---- .../service/NostrAccountDataSource.kt | 2 +- .../amethyst/ui/actions/NewPostViewModel.kt | 12 ++++- .../amethyst/ui/note/ChannelCardCompose.kt | 4 +- .../ui/note/ChatroomMessageCompose.kt | 21 +++++++- .../amethyst/ui/note/NoteCompose.kt | 6 +-- .../amethyst/ui/note/NoteQuickActionMenu.kt | 53 +++++++++++++++++-- .../amethyst/ui/note/elements/DropDownMenu.kt | 2 +- .../amethyst/ui/screen/ChatroomFeedView.kt | 7 +++ .../amethyst/ui/screen/ThreadFeedView.kt | 2 +- .../ui/screen/loggedIn/ChannelScreen.kt | 8 ++- .../ui/screen/loggedIn/ChatroomScreen.kt | 8 ++- 14 files changed, 150 insertions(+), 35 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 619e70347..f3045fdbf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -1369,7 +1369,7 @@ class Account( Client.send(draftEvent, relayList = relayList) LocalCache.justConsume(draftEvent, null) LocalCache.justConsume(it, null) - LocalCache.addDraft(draftTag, it.id()) + LocalCache.addDraft(draftTag, draftEvent.id(), it.id()) } } else { Client.send(it, relayList = relayList) @@ -1431,7 +1431,7 @@ class Account( Client.send(draftEvent, relayList = relayList) LocalCache.justConsume(draftEvent, null) LocalCache.justConsume(it, null) - LocalCache.addDraft(draftTag, it.id()) + LocalCache.addDraft(draftTag, draftEvent.id(), it.id()) } } else { Client.send(it, relayList = relayList) @@ -1499,7 +1499,7 @@ class Account( Client.send(draftEvent, relayList = relayList) LocalCache.justConsume(draftEvent, null) LocalCache.justConsume(it, null) - LocalCache.addDraft(draftTag, it.id()) + LocalCache.addDraft(draftTag, draftEvent.id(), it.id()) } } else { Client.send(it, relayList = relayList) @@ -1590,7 +1590,7 @@ class Account( Client.send(draftEvent, relayList = relayList) LocalCache.justConsume(draftEvent, null) LocalCache.justConsume(it, null) - LocalCache.addDraft(draftTag, it.id()) + LocalCache.addDraft(draftTag, draftEvent.id(), it.id()) } } else { Client.send(it, relayList = relayList) @@ -1642,7 +1642,7 @@ class Account( Client.send(draftEvent) LocalCache.justConsume(draftEvent, null) LocalCache.justConsume(it, null) - LocalCache.addDraft(draftTag, it.id()) + LocalCache.addDraft(draftTag, draftEvent.id(), it.id()) } } else { Client.send(it) @@ -1687,7 +1687,7 @@ class Account( Client.send(draftEvent) LocalCache.justConsume(draftEvent, null) LocalCache.justConsume(it, null) - LocalCache.addDraft(draftTag, it.id()) + LocalCache.addDraft(draftTag, draftEvent.id(), it.id()) } } else { Client.send(it) @@ -1759,7 +1759,7 @@ class Account( Client.send(draftEvent) LocalCache.justConsume(draftEvent, null) LocalCache.justConsume(it, null) - LocalCache.addDraft(draftTag, it.id()) + LocalCache.addDraft(draftTag, draftEvent.id(), it.id()) } } else { Client.send(it) @@ -1805,7 +1805,7 @@ class Account( Client.send(draftEvent) LocalCache.justConsume(draftEvent, null) LocalCache.justConsume(it.msg, null) - LocalCache.addDraft(draftTag, it.msg.id()) + LocalCache.addDraft(draftTag, draftEvent.id(), it.msg.id()) } } else { broadcastPrivately(it) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Drafts.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Drafts.kt index f01d30a16..9dc3b1c44 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Drafts.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Drafts.kt @@ -1,2 +1,23 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ package com.vitorpamplona.amethyst.model +data class Drafts(val mainId: String, val eventId: String) 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 3e6a8309d..4ff2fd4f7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -129,7 +129,7 @@ object LocalCache { val users = LargeCache() val notes = LargeCache() val addressables = LargeCache() - val drafts = ConcurrentHashMap>() + val drafts = ConcurrentHashMap>() val channels = ConcurrentHashMap() val awaitingPaymentRequests = ConcurrentHashMap Unit>>(10) @@ -144,25 +144,30 @@ object LocalCache { fun draftNotes(draftTag: String): List { return drafts[draftTag]?.mapNotNull { - checkGetOrCreateNote(it) + getNoteIfExists(it.mainId) } ?: listOf() } fun getDrafts(eventId: String): List { return drafts.filter { - it.value.any { it == eventId } - }.keys.mapNotNull { - checkGetOrCreateNote(it) - } + it.value.any { it.eventId == eventId } + }.values.map { + it.mapNotNull { + checkGetOrCreateNote(it.mainId) + } + }.flatten() } fun addDraft( key: String, - value: String, + mainId: String, + draftId: String, ) { val data = drafts[key] ?: mutableListOf() - data.add(value) - drafts[key] = data + if (data.none { it.mainId == mainId }) { + data.add(Drafts(mainId, draftId)) + drafts[key] = data + } } fun getOrCreateUser(key: HexKey): User { 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 5507975b7..34a51b4c6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt @@ -289,7 +289,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") { LocalCache.justConsume(it, relay) tag?.let { lTag -> - LocalCache.addDraft(lTag, it.id()) + LocalCache.addDraft(lTag, event.id(), it.id()) } } } 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 a177a0b9c..2b48befa3 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 @@ -315,7 +315,7 @@ open class NewPostViewModel() : ViewModel() { Log.d("draft", draft.event!!.toJson()) draftTag = LocalCache.drafts.filter { - it.value.contains(draft.idHex) + it.value.any { it.eventId == draft.event?.id() } }.keys.firstOrNull() ?: draftTag canAddInvoice = accountViewModel.userProfile().info?.lnAddress() != null @@ -394,7 +394,15 @@ open class NewPostViewModel() : ViewModel() { it.value == draft.event?.tags()?.filter { it.size > 1 && it[0] == "condition" }?.map { it[1] }?.firstOrNull() } ?: ClassifiedsEvent.CONDITION.USED_LIKE_NEW - message = TextFieldValue(draft.event?.content() ?: "") + message = + if (draft.event is PrivateDmEvent) { + val event = draft.event as PrivateDmEvent + TextFieldValue(event.cachedContentFor(accountViewModel.account.signer) ?: "") + } else { + TextFieldValue(draft.event?.content() ?: "") + } + + nip24 = draft.event is ChatMessageEvent urlPreview = findUrlInMessage() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt index 251dfac0c..d701a8fc9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt @@ -134,7 +134,7 @@ fun ChannelCardCompose( ) } } else { - LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup, + LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel, newPostViewModel = null) { showPopup, -> BlankNote( remember { @@ -281,7 +281,7 @@ fun NormalChannelCard( accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup -> + LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel, newPostViewModel = null) { showPopup -> CheckNewAndRenderChannelCard( baseNote, routeForLastRead, 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 5d67af21f..133d9833c 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 @@ -66,6 +66,7 @@ import androidx.lifecycle.map import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.components.SensitivityWarning @@ -105,6 +106,7 @@ fun ChatroomMessageCompose( innerQuote: Boolean = false, parentBackgroundColor: MutableState? = null, accountViewModel: AccountViewModel, + newPostViewModel: NewPostViewModel?, nav: (String) -> Unit, onWantsToReply: (Note) -> Unit, ) { @@ -118,11 +120,12 @@ fun ChatroomMessageCompose( innerQuote, parentBackgroundColor, accountViewModel, + newPostViewModel, nav, onWantsToReply, ) } else { - LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup, + LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel, newPostViewModel = newPostViewModel) { showPopup, -> BlankNote( remember { @@ -144,6 +147,7 @@ fun CheckHiddenChatMessage( innerQuote: Boolean = false, parentBackgroundColor: MutableState? = null, accountViewModel: AccountViewModel, + newPostViewModel: NewPostViewModel?, nav: (String) -> Unit, onWantsToReply: (Note) -> Unit, ) { @@ -162,6 +166,7 @@ fun CheckHiddenChatMessage( innerQuote, parentBackgroundColor, accountViewModel, + newPostViewModel, nav, onWantsToReply, ) @@ -175,6 +180,7 @@ fun LoadedChatMessageCompose( innerQuote: Boolean = false, parentBackgroundColor: MutableState? = null, accountViewModel: AccountViewModel, + newPostViewModel: NewPostViewModel?, nav: (String) -> Unit, onWantsToReply: (Note) -> Unit, ) { @@ -220,6 +226,7 @@ fun LoadedChatMessageCompose( canPreview, parentBackgroundColor, accountViewModel, + newPostViewModel, nav, onWantsToReply, ) @@ -236,6 +243,7 @@ fun NormalChatNote( canPreview: Boolean = true, parentBackgroundColor: MutableState? = null, accountViewModel: AccountViewModel, + newPostViewModel: NewPostViewModel?, nav: (String) -> Unit, onWantsToReply: (Note) -> Unit, ) { @@ -347,6 +355,7 @@ fun NormalChatNote( availableBubbleSize, showDetails, accountViewModel, + newPostViewModel, nav, ) } @@ -357,6 +366,7 @@ fun NormalChatNote( popupExpanded = popupExpanded, onDismiss = { popupExpanded = false }, accountViewModel = accountViewModel, + newPostViewModel = newPostViewModel, ) } } @@ -374,6 +384,7 @@ private fun RenderBubble( availableBubbleSize: MutableState, showDetails: State, accountViewModel: AccountViewModel, + newPostViewModel: NewPostViewModel?, nav: (String) -> Unit, ) { val bubbleSize = remember { mutableIntStateOf(0) } @@ -403,6 +414,7 @@ private fun RenderBubble( canPreview, showDetails, accountViewModel, + newPostViewModel, nav, ) } @@ -421,6 +433,7 @@ private fun MessageBubbleLines( canPreview: Boolean, showDetails: State, accountViewModel: AccountViewModel, + newPostViewModel: NewPostViewModel?, nav: (String) -> Unit, ) { if (drawAuthorInfo) { @@ -437,6 +450,7 @@ private fun MessageBubbleLines( innerQuote = innerQuote, backgroundBubbleColor = backgroundBubbleColor, accountViewModel = accountViewModel, + newPostViewModel = newPostViewModel, nav = nav, onWantsToReply = onWantsToReply, ) @@ -489,11 +503,12 @@ private fun RenderReplyRow( innerQuote: Boolean, backgroundBubbleColor: MutableState, accountViewModel: AccountViewModel, + newPostViewModel: NewPostViewModel?, nav: (String) -> Unit, onWantsToReply: (Note) -> Unit, ) { if (!innerQuote && note.replyTo?.lastOrNull() != null) { - RenderReply(note, backgroundBubbleColor, accountViewModel, nav, onWantsToReply) + RenderReply(note, backgroundBubbleColor, accountViewModel, newPostViewModel, nav, onWantsToReply) } } @@ -502,6 +517,7 @@ private fun RenderReply( note: Note, backgroundBubbleColor: MutableState, accountViewModel: AccountViewModel, + newPostViewModel: NewPostViewModel?, nav: (String) -> Unit, onWantsToReply: (Note) -> Unit, ) { @@ -520,6 +536,7 @@ private fun RenderReply( innerQuote = true, parentBackgroundColor = backgroundBubbleColor, accountViewModel = accountViewModel, + newPostViewModel = newPostViewModel, nav = nav, onWantsToReply = onWantsToReply, ) 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 8780d7dd0..93f6c41af 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 @@ -239,7 +239,7 @@ fun WatchNoteEvent( if (it) { onNoteEventFound() } else { - LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup -> + LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel, newPostViewModel = null) { showPopup -> BlankNote( remember { modifier.combinedClickable( @@ -414,7 +414,7 @@ fun AcceptableNote( nav = nav, ) else -> - LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { + LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel, newPostViewModel = null) { showPopup, -> CheckNewAndRenderNote( @@ -450,7 +450,7 @@ fun AcceptableNote( is FileHeaderEvent -> FileHeaderDisplay(baseNote, false, accountViewModel) is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, false, accountViewModel) else -> - LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { + LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel, newPostViewModel = null) { showPopup, -> CheckNewAndRenderNote( 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 a60b15af4..97542e84e 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 @@ -40,6 +40,7 @@ import androidx.compose.material.icons.filled.AlternateEmail import androidx.compose.material.icons.filled.Block import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.FormatQuote import androidx.compose.material.icons.filled.PersonAdd import androidx.compose.material.icons.filled.PersonRemove @@ -84,6 +85,8 @@ import androidx.core.graphics.ColorUtils import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.actions.NewPostView +import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel import com.vitorpamplona.amethyst.ui.components.SelectTextDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog @@ -132,6 +135,7 @@ val externalLinkForNote = { note: Note -> fun LongPressToQuickAction( baseNote: Note, accountViewModel: AccountViewModel, + newPostViewModel: NewPostViewModel?, content: @Composable (() -> Unit) -> Unit, ) { val popupExpanded = remember { mutableStateOf(false) } @@ -140,7 +144,7 @@ fun LongPressToQuickAction( content(showPopup) - NoteQuickActionMenu(baseNote, popupExpanded.value, hidePopup, accountViewModel) + NoteQuickActionMenu(baseNote, popupExpanded.value, hidePopup, accountViewModel, newPostViewModel) } @Composable @@ -149,20 +153,24 @@ fun NoteQuickActionMenu( popupExpanded: Boolean, onDismiss: () -> Unit, accountViewModel: AccountViewModel, + newPostViewModel: NewPostViewModel?, ) { val showSelectTextDialog = remember { mutableStateOf(false) } val showDeleteAlertDialog = remember { mutableStateOf(false) } val showBlockAlertDialog = remember { mutableStateOf(false) } val showReportDialog = remember { mutableStateOf(false) } + val editDraftDialog = remember { mutableStateOf(false) } if (popupExpanded) { RenderMainPopup( accountViewModel, + newPostViewModel, note, onDismiss, showBlockAlertDialog, showDeleteAlertDialog, showReportDialog, + editDraftDialog, ) } @@ -199,16 +207,29 @@ fun NoteQuickActionMenu( onDismiss() } } + + if (editDraftDialog.value) { + NewPostView( + onClose = { + editDraftDialog.value = false + }, + accountViewModel = accountViewModel, + draft = note, + nav = { }, + ) + } } @Composable private fun RenderMainPopup( accountViewModel: AccountViewModel, + newPostViewModel: NewPostViewModel?, note: Note, onDismiss: () -> Unit, showBlockAlertDialog: MutableState, showDeleteAlertDialog: MutableState, showReportDialog: MutableState, + editDraftDialog: MutableState, ) { val context = LocalContext.current val primaryLight = lightenColor(MaterialTheme.colorScheme.primary, 0.1f) @@ -279,6 +300,21 @@ private fun RenderMainPopup( } } + if (note.isDraft()) { + VerticalDivider(color = primaryLight) + NoteQuickActionItem( + Icons.Default.Edit, + stringResource(R.string.edit_draft), + ) { + if (newPostViewModel != null) { + newPostViewModel.load(accountViewModel, null, null, null, null, note) + onDismiss() + } else { + editDraftDialog.value = true + } + } + } + if (!isOwnNote) { VerticalDivider(color = primaryLight) @@ -389,14 +425,20 @@ fun NoteQuickActionItem( onClick: () -> Unit, ) { Column( - modifier = Modifier.size(70.dp).clickable { onClick() }, + modifier = + Modifier + .size(70.dp) + .clickable { onClick() }, verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, ) { Icon( imageVector = icon, contentDescription = null, - modifier = Modifier.size(24.dp).padding(bottom = 5.dp), + modifier = + Modifier + .size(24.dp) + .padding(bottom = 5.dp), tint = Color.White, ) Text(text = label, fontSize = 12.sp, color = Color.White, textAlign = TextAlign.Center) @@ -527,7 +569,10 @@ fun QuickActionAlertDialog( text = { Text(textContent) }, confirmButton = { Row( - modifier = Modifier.padding(all = 8.dp).fillMaxWidth(), + modifier = + Modifier + .padding(all = 8.dp) + .fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, ) { TextButton(onClick = onClickDontShowAgain) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt index 70cc123c2..9683bb3c4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt @@ -237,7 +237,7 @@ fun NoteDropDownMenu( }, ) HorizontalDivider(thickness = DividerThickness) - if (!note.isDraft()) { + if (note.isDraft()) { DropdownMenuItem( text = { Text(stringResource(R.string.edit_draft)) }, onClick = { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt index 89c5c6b52..83760f6a0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ChatroomFeedView.kt @@ -38,6 +38,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.FeedPadding @@ -48,6 +49,7 @@ import com.vitorpamplona.amethyst.ui.theme.HalfPadding fun RefreshingChatroomFeedView( viewModel: FeedViewModel, accountViewModel: AccountViewModel, + newPostViewModel: NewPostViewModel, nav: (String) -> Unit, routeForLastRead: String, onWantsToReply: (Note) -> Unit, @@ -59,6 +61,7 @@ fun RefreshingChatroomFeedView( RenderChatroomFeedView( viewModel, accountViewModel, + newPostViewModel, listState, nav, routeForLastRead, @@ -72,6 +75,7 @@ fun RefreshingChatroomFeedView( fun RenderChatroomFeedView( viewModel: FeedViewModel, accountViewModel: AccountViewModel, + newPostViewModel: NewPostViewModel, listState: LazyListState, nav: (String) -> Unit, routeForLastRead: String, @@ -91,6 +95,7 @@ fun RenderChatroomFeedView( ChatroomFeedLoaded( state, accountViewModel, + newPostViewModel, listState, nav, routeForLastRead, @@ -108,6 +113,7 @@ fun RenderChatroomFeedView( fun ChatroomFeedLoaded( state: FeedState.Loaded, accountViewModel: AccountViewModel, + newPostViewModel: NewPostViewModel, listState: LazyListState, nav: (String) -> Unit, routeForLastRead: String, @@ -130,6 +136,7 @@ fun ChatroomFeedLoaded( baseNote = item, routeForLastRead = routeForLastRead, accountViewModel = accountViewModel, + newPostViewModel = newPostViewModel, nav = nav, onWantsToReply = onWantsToReply, ) 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 c3cae7289..803b9f030 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 @@ -637,7 +637,7 @@ fun NoteMaster( ) } - NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel) + NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel, null) } } 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 d98153f89..ae1be0d8f 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 @@ -175,6 +175,7 @@ import java.text.DateFormat import java.text.SimpleDateFormat import java.util.Date import java.util.Locale +import java.util.UUID @Composable fun ChannelScreen( @@ -307,6 +308,7 @@ fun ChannelScreen( RefreshingChatroomFeedView( viewModel = feedViewModel, accountViewModel = accountViewModel, + newPostViewModel = newPostModel, nav = nav, routeForLastRead = "Channel/${channel.idHex}", onWantsToReply = { replyTo.value = it }, @@ -315,7 +317,7 @@ fun ChannelScreen( Spacer(modifier = DoubleVertSpacer) - replyTo.value?.let { DisplayReplyingToNote(it, accountViewModel, nav) { replyTo.value = null } } + replyTo.value?.let { DisplayReplyingToNote(it, accountViewModel, newPostModel, nav) { replyTo.value = null } } val scope = rememberCoroutineScope() @@ -358,6 +360,8 @@ fun ChannelScreen( } newPostModel.message = TextFieldValue("") replyTo.value = null + accountViewModel.deleteDraft(newPostModel.draftTag) + newPostModel.draftTag = UUID.randomUUID().toString() feedViewModel.sendToTop() } } @@ -368,6 +372,7 @@ fun ChannelScreen( fun DisplayReplyingToNote( replyingNote: Note?, accountViewModel: AccountViewModel, + newPostModel: NewPostViewModel, nav: (String) -> Unit, onCancel: () -> Unit, ) { @@ -386,6 +391,7 @@ fun DisplayReplyingToNote( null, innerQuote = true, accountViewModel = accountViewModel, + newPostViewModel = newPostModel, nav = nav, onWantsToReply = {}, ) 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 da63ece2d..516594197 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 @@ -130,6 +130,7 @@ import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.util.UUID @Composable fun ChatroomScreen( @@ -332,6 +333,7 @@ fun ChatroomScreen( RefreshingChatroomFeedView( viewModel = feedViewModel, accountViewModel = accountViewModel, + newPostViewModel = newPostModel, nav = nav, routeForLastRead = "Room/${room.hashCode()}", onWantsToReply = { @@ -343,13 +345,16 @@ fun ChatroomScreen( Spacer(modifier = Modifier.height(10.dp)) - replyTo.value?.let { DisplayReplyingToNote(it, accountViewModel, nav) { replyTo.value = null } } + replyTo.value?.let { DisplayReplyingToNote(it, accountViewModel, newPostModel, nav) { replyTo.value = null } } val scope = rememberCoroutineScope() // LAST ROW PrivateMessageEditFieldRow(newPostModel, isPrivate = true, accountViewModel) { scope.launch(Dispatchers.IO) { + accountViewModel.deleteDraft(newPostModel.draftTag) + newPostModel.draftTag = UUID.randomUUID().toString() + val urls = findURLs(newPostModel.message.text) val usedAttachments = newPostModel.nip94attachments.filter { it.urls().intersect(urls.toSet()).isNotEmpty() } @@ -361,6 +366,7 @@ fun ChatroomScreen( mentions = null, wantsToMarkAsSensitive = false, nip94attachments = usedAttachments, + draftTag = null, ) } else { accountViewModel.account.sendPrivateMessage( From 644d2fc2bb1d7b5383a61491373d364514db8e8d Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Wed, 20 Mar 2024 14:56:29 -0300 Subject: [PATCH 30/32] add generic draft event to default permissions --- .../vitorpamplona/quartz/signers/ExternalSignerLauncher.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/signers/ExternalSignerLauncher.kt b/quartz/src/main/java/com/vitorpamplona/quartz/signers/ExternalSignerLauncher.kt index 0d9c1f252..8b0cad500 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/signers/ExternalSignerLauncher.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/signers/ExternalSignerLauncher.kt @@ -171,6 +171,10 @@ class ExternalSignerLauncher( "sign_event", 22242, ), + Permission( + "sign_event", + 31234, + ), Permission( "nip04_encrypt", ), From 27db2b91ab565e94050721180bb9478db9f1c607 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Mon, 25 Mar 2024 07:32:31 -0300 Subject: [PATCH 31/32] save draft when changing options from polls and classifieds --- .../amethyst/ui/actions/NewPollOption.kt | 17 +++++++-- .../amethyst/ui/actions/NewPostView.kt | 35 ++++++++++++++++--- .../amethyst/ui/actions/NewPostViewModel.kt | 6 ++++ 3 files changed, 52 insertions(+), 6 deletions(-) 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 6f8378cc0..794fa5cbf 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 @@ -34,8 +34,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.theme.placeholderText +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch @Composable fun NewPollOption( @@ -45,7 +48,12 @@ fun NewPollOption( Row { val deleteIcon: @Composable (() -> Unit) = { IconButton( - onClick = { pollViewModel.pollOptions.remove(optionIndex) }, + onClick = { + pollViewModel.pollOptions.remove(optionIndex) + pollViewModel.viewModelScope.launch(Dispatchers.IO) { + pollViewModel.saveDraft() + } + }, ) { Icon( imageVector = Icons.Default.Delete, @@ -57,7 +65,12 @@ fun NewPollOption( OutlinedTextField( modifier = Modifier.weight(1F), value = pollViewModel.pollOptions[optionIndex] ?: "", - onValueChange = { pollViewModel.pollOptions[optionIndex] = it }, + onValueChange = { + pollViewModel.pollOptions[optionIndex] = it + pollViewModel.viewModelScope.launch(Dispatchers.IO) { + pollViewModel.saveDraft() + } + }, label = { Text( text = stringResource(R.string.poll_option_index).format(optionIndex + 1), 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 3c9cbc915..6ac20c9c3 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 @@ -119,6 +119,7 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage import com.google.accompanist.permissions.ExperimentalPermissionsApi @@ -596,6 +597,9 @@ private fun BottomRowActions(postViewModel: NewPostViewModel) { MarkAsSensitive(postViewModel) { postViewModel.wantsToMarkAsSensitive = !postViewModel.wantsToMarkAsSensitive + postViewModel.viewModelScope.launch(Dispatchers.IO) { + postViewModel.saveDraft() + } } AddGeoHash(postViewModel) { @@ -841,7 +845,12 @@ fun SellProduct(postViewModel: NewPostViewModel) { MyTextField( value = postViewModel.title, - onValueChange = { postViewModel.title = it }, + onValueChange = { + postViewModel.title = it + postViewModel.viewModelScope.launch(Dispatchers.IO) { + postViewModel.saveDraft() + } + }, modifier = Modifier.fillMaxWidth(), placeholder = { Text( @@ -884,6 +893,9 @@ fun SellProduct(postViewModel: NewPostViewModel) { postViewModel.price = it } } + postViewModel.viewModelScope.launch(Dispatchers.IO) { + postViewModel.saveDraft() + } }, placeholder = { Text( @@ -948,7 +960,12 @@ fun SellProduct(postViewModel: NewPostViewModel) { TextSpinner( placeholder = conditionTypes.filter { it.first == postViewModel.condition }.first().second, options = conditionOptions, - onSelect = { postViewModel.condition = conditionTypes[it].first }, + onSelect = { + postViewModel.condition = conditionTypes[it].first + postViewModel.viewModelScope.launch(Dispatchers.IO) { + postViewModel.saveDraft() + } + }, modifier = Modifier .weight(1f) @@ -1012,7 +1029,12 @@ fun SellProduct(postViewModel: NewPostViewModel) { categoryTypes.filter { it.second == postViewModel.category.text }.firstOrNull()?.second ?: "", options = categoryOptions, - onSelect = { postViewModel.category = TextFieldValue(categoryTypes[it].second) }, + onSelect = { + postViewModel.category = TextFieldValue(categoryTypes[it].second) + postViewModel.viewModelScope.launch(Dispatchers.IO) { + postViewModel.saveDraft() + } + }, modifier = Modifier .weight(1f) @@ -1047,7 +1069,12 @@ fun SellProduct(postViewModel: NewPostViewModel) { MyTextField( value = postViewModel.locationText, - onValueChange = { postViewModel.locationText = it }, + onValueChange = { + postViewModel.locationText = it + postViewModel.viewModelScope.launch(Dispatchers.IO) { + postViewModel.saveDraft() + } + }, modifier = Modifier.fillMaxWidth(), placeholder = { Text( 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 2b48befa3..f9c6eedab 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 @@ -1134,6 +1134,9 @@ open class NewPostViewModel() : ViewModel() { } checkMinMax() + viewModelScope.launch(Dispatchers.IO) { + saveDraft() + } } fun updateMaxZapAmountForPoll(textMax: String) { @@ -1153,6 +1156,9 @@ open class NewPostViewModel() : ViewModel() { } checkMinMax() + viewModelScope.launch(Dispatchers.IO) { + saveDraft() + } } fun checkMinMax() { From ed4d86762285043f2c4ce83f730dda95b93a08a9 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Mon, 25 Mar 2024 07:48:30 -0300 Subject: [PATCH 32/32] save draft when toogling nip4 and nip 44 --- .../vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt | 5 +++++ 1 file changed, 5 insertions(+) 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 f9c6eedab..e2abf4c81 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 @@ -1115,6 +1115,11 @@ open class NewPostViewModel() : ViewModel() { } else { nip24 = !nip24 } + if (message.text.isNotBlank()) { + viewModelScope.launch(Dispatchers.IO) { + saveDraft() + } + } } fun updateMinZapAmountForPoll(textMin: String) {