From 8344274011434bc1dd4451ca5064a9b160591a17 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 6 Apr 2023 11:54:28 -0400 Subject: [PATCH] Adds Invoice Creation to New Posts. --- .../com/vitorpamplona/amethyst/model/User.kt | 4 + .../amethyst/ui/actions/NewMessageTagger.kt | 80 +++++++++++++++++++ .../amethyst/ui/actions/NewPostView.kt | 60 ++++++++++++++ .../amethyst/ui/actions/NewPostViewModel.kt | 12 ++- .../amethyst/ui/components/InvoiceRequest.kt | 25 +++--- .../ui/screen/loggedIn/ProfileScreen.kt | 24 +++++- app/src/main/res/values/strings.xml | 2 + 7 files changed, 190 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt index 909a4d705..476f4ef7d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -387,6 +387,10 @@ class UserMetadata { return listOfNotNull(name, username, display_name, displayName, nip05, lud06, lud16) .any { it.startsWith(prefix, true) } } + + fun lnAddress(): String? { + return (lud16?.trim() ?: lud06?.trim())?.ifBlank { null } + } } class UserLiveData(val user: User) : LiveData(UserState(user)) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt new file mode 100644 index 000000000..dc39228e0 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt @@ -0,0 +1,80 @@ +package com.vitorpamplona.amethyst.ui.actions + +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.parseDirtyWordForKey +import com.vitorpamplona.amethyst.service.nip19.Nip19 + +class NewMessageProcessor(var originalNote: Note?, var mentions: List?, var replyTos: List?, var message: String) { + + open fun addUserToMentions(user: User) { + mentions = if (mentions?.contains(user) == true) mentions else mentions?.plus(user) ?: listOf(user) + } + + open fun addNoteToReplyTos(note: Note) { + note.author?.let { addUserToMentions(it) } + replyTos = if (replyTos?.contains(note) == true) replyTos else replyTos?.plus(note) ?: listOf(note) + } + + open fun tagIndex(user: User): Int { + // Postr Events assembles replies before mentions in the tag order + return (if (originalNote?.channel() != null) 1 else 0) + (replyTos?.size ?: 0) + (mentions?.indexOf(user) ?: 0) + } + + open fun tagIndex(note: Note): Int { + // Postr Events assembles replies before mentions in the tag order + return (if (originalNote?.channel() != null) 1 else 0) + (replyTos?.indexOf(note) ?: 0) + } + + fun run() { + // adds all references to mentions and reply tos + message.split('\n').forEach { paragraph: String -> + paragraph.split(' ').forEach { word: String -> + val results = parseDirtyWordForKey(word) + + if (results?.key?.type == Nip19.Type.USER) { + addUserToMentions(LocalCache.getOrCreateUser(results.key.hex)) + } else if (results?.key?.type == Nip19.Type.NOTE) { + addNoteToReplyTos(LocalCache.getOrCreateNote(results.key.hex)) + } else if (results?.key?.type == Nip19.Type.EVENT) { + addNoteToReplyTos(LocalCache.getOrCreateNote(results.key.hex)) + } else if (results?.key?.type == Nip19.Type.ADDRESS) { + val note = LocalCache.checkGetOrCreateAddressableNote(results.key.hex) + if (note != null) { + addNoteToReplyTos(note) + } + } + } + } + + // Tags the text in the correct order. + message = message.split('\n').map { paragraph: String -> + paragraph.split(' ').map { word: String -> + val results = parseDirtyWordForKey(word) + if (results?.key?.type == Nip19.Type.USER) { + val user = LocalCache.getOrCreateUser(results.key.hex) + + "#[${tagIndex(user)}]${results.restOfWord}" + } else if (results?.key?.type == Nip19.Type.NOTE) { + val note = LocalCache.getOrCreateNote(results.key.hex) + + "#[${tagIndex(note)}]${results.restOfWord}" + } else if (results?.key?.type == Nip19.Type.EVENT) { + val note = LocalCache.getOrCreateNote(results.key.hex) + + "#[${tagIndex(note)}]${results.restOfWord}" + } else if (results?.key?.type == Nip19.Type.ADDRESS) { + val note = LocalCache.checkGetOrCreateAddressableNote(results.key.hex) + if (note != null) { + "#[${tagIndex(note)}]${results.restOfWord}" + } else { + word + } + } else { + word + } + }.joinToString(" ") + }.joinToString("\n") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index fba0b3fa1..8549dd0ec 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -12,6 +12,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MonetizationOn import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember @@ -29,6 +31,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog @@ -182,6 +185,28 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n } } + val user = postViewModel.account?.userProfile() + val lud16 = user?.info?.lnAddress() + + if (lud16 != null && user != null && postViewModel.wantsInvoice) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 5.dp)) { + InvoiceRequest( + lud16, + user.pubkeyHex, + account, + stringResource(id = R.string.lightning_invoice), + stringResource(id = R.string.lightning_create_and_add_invoice), + onSuccess = { + postViewModel.message = TextFieldValue(postViewModel.message.text + "\n\n" + it) + postViewModel.wantsInvoice = false + }, + onClose = { + postViewModel.wantsInvoice = false + } + ) + } + } + val myUrlPreview = postViewModel.urlPreview if (myUrlPreview != null) { Row(modifier = Modifier.padding(top = 5.dp)) { @@ -249,6 +274,12 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n postViewModel.wantsPoll = !postViewModel.wantsPoll } } + + if (postViewModel.canAddLnInvoice()) { + AddLnInvoiceButton(postViewModel.wantsInvoice) { + postViewModel.wantsInvoice = !postViewModel.wantsInvoice + } + } } } } @@ -284,6 +315,35 @@ private fun AddPollButton( } } + +@Composable +private fun AddLnInvoiceButton( + isLnInvoiceActive: Boolean, + onClick: () -> Unit +) { + IconButton( + onClick = { + onClick() + } + ) { + if (!isLnInvoiceActive) { + Icon( + imageVector = Icons.Default.MonetizationOn, + null, + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colors.onBackground + ) + } else { + Icon( + imageVector = Icons.Default.MonetizationOn, + null, + modifier = Modifier.size(20.dp), + tint = Color.Green.copy(alpha = 0.52f) + ) + } + } +} + @Composable fun CloseButton(onCancel: () -> Unit) { Button( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index 21f479931..fbb964c5b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -53,6 +53,10 @@ open class NewPostViewModel : ViewModel() { var isValidConsensusThreshold = mutableStateOf(true) var isValidClosedAt = mutableStateOf(true) + // Invoices + var wantsInvoice by mutableStateOf(false) + + open fun load(account: Account, replyingTo: Note?, quote: Note?) { originalNote = replyingTo replyingTo?.let { replyNote -> @@ -199,6 +203,8 @@ open class NewPostViewModel : ViewModel() { valueMinimum = null consensusThreshold = null closedAt = null + + wantsInvoice = false } open fun findUrlInMessage(): String? { @@ -248,11 +254,15 @@ open class NewPostViewModel : ViewModel() { } fun canPost(): Boolean { - return message.text.isNotBlank() && !isUploadingImage && + return message.text.isNotBlank() && !isUploadingImage && !wantsInvoice && (!wantsPoll || pollOptions.values.all { it.isNotEmpty() }) } fun canUsePoll(): Boolean { return originalNote?.event !is PrivateDmEvent && originalNote?.channel() == null } + + fun canAddLnInvoice(): Boolean { + return account?.userProfile()?.info?.lnAddress() != null + } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt index 2fff33f83..bca274b3c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt @@ -1,7 +1,5 @@ package com.vitorpamplona.amethyst.ui.components -import android.content.Intent -import android.net.Uri import android.widget.Toast import androidx.compose.foundation.border import androidx.compose.foundation.layout.Column @@ -36,14 +34,21 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.core.content.ContextCompat.startActivity import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver import kotlinx.coroutines.launch @Composable -fun InvoiceRequest(lud16: String, toUserPubKeyHex: String, account: Account, onClose: () -> Unit) { +fun InvoiceRequest( + lud16: String, + toUserPubKeyHex: String, + account: Account, + titleText: String? = null, + buttonText: String? = null, + onSuccess: (String) -> Unit, + onClose: () -> Unit +) { val context = LocalContext.current val scope = rememberCoroutineScope() @@ -73,7 +78,7 @@ fun InvoiceRequest(lud16: String, toUserPubKeyHex: String, account: Account, onC ) Text( - text = stringResource(R.string.lightning_tips), + text = titleText ?: stringResource(R.string.lightning_tips), fontSize = 20.sp, fontWeight = FontWeight.W500, modifier = Modifier.padding(start = 10.dp) @@ -137,13 +142,7 @@ fun InvoiceRequest(lud16: String, toUserPubKeyHex: String, account: Account, onC amount * 1000, message, zapRequest?.toJson(), - onSuccess = { - runCatching { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$it")) - startActivity(context, intent, null) - } - onClose() - }, + onSuccess = onSuccess, onError = { scope.launch { Toast.makeText(context, it, Toast.LENGTH_SHORT).show() @@ -159,7 +158,7 @@ fun InvoiceRequest(lud16: String, toUserPubKeyHex: String, account: Account, onC backgroundColor = MaterialTheme.colors.primary ) ) { - Text(text = stringResource(R.string.send_sats), color = Color.White, fontSize = 20.sp) + Text(text = buttonText ?: stringResource(R.string.send_sats), color = Color.White, fontSize = 20.sp) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index 8f262e310..3f60fbb90 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -1,5 +1,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn +import android.content.Intent +import android.net.Uri import androidx.compose.foundation.* import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.layout.* @@ -27,6 +29,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalUriHandler @@ -39,6 +42,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.core.content.ContextCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.viewmodel.compose.viewModel @@ -421,6 +425,7 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account, accountViewMode val uri = LocalUriHandler.current val clipboardManager = LocalClipboardManager.current + val context = LocalContext.current Row(verticalAlignment = Alignment.Bottom) { user.bestDisplayName()?.let { @@ -555,9 +560,22 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account, accountViewMode if (zapExpanded) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 5.dp)) { - InvoiceRequest(lud16, baseUser.pubkeyHex, account) { - zapExpanded = false - } + InvoiceRequest(lud16, baseUser.pubkeyHex, account, + onSuccess = { + // pay directly + if (account.hasWalletConnectSetup()) { + account.sendZapPaymentRequestFor(it) + } else { + runCatching { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$it")) + ContextCompat.startActivity(context, intent, null) + } + } + }, + onClose = { + zapExpanded = false + } + ) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 40e656b03..6e8947047 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -288,4 +288,6 @@ Add a public message Thank you for all your work! + + Create and Add