Adds Invoice Creation to New Posts.

This commit is contained in:
Vitor Pamplona 2023-04-06 11:54:28 -04:00
parent b0953310c2
commit 8344274011
7 changed files with 190 additions and 17 deletions

View File

@ -387,6 +387,10 @@ class UserMetadata {
return listOfNotNull(name, username, display_name, displayName, nip05, lud06, lud16) return listOfNotNull(name, username, display_name, displayName, nip05, lud06, lud16)
.any { it.startsWith(prefix, true) } .any { it.startsWith(prefix, true) }
} }
fun lnAddress(): String? {
return (lud16?.trim() ?: lud06?.trim())?.ifBlank { null }
}
} }
class UserLiveData(val user: User) : LiveData<UserState>(UserState(user)) { class UserLiveData(val user: User) : LiveData<UserState>(UserState(user)) {

View File

@ -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<User>?, var replyTos: List<Note>?, 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")
}
}

View File

@ -12,6 +12,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.* 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.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember 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.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardCapitalization 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.TextDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog 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 val myUrlPreview = postViewModel.urlPreview
if (myUrlPreview != null) { if (myUrlPreview != null) {
Row(modifier = Modifier.padding(top = 5.dp)) { 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 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 @Composable
fun CloseButton(onCancel: () -> Unit) { fun CloseButton(onCancel: () -> Unit) {
Button( Button(

View File

@ -53,6 +53,10 @@ open class NewPostViewModel : ViewModel() {
var isValidConsensusThreshold = mutableStateOf(true) var isValidConsensusThreshold = mutableStateOf(true)
var isValidClosedAt = mutableStateOf(true) var isValidClosedAt = mutableStateOf(true)
// Invoices
var wantsInvoice by mutableStateOf(false)
open fun load(account: Account, replyingTo: Note?, quote: Note?) { open fun load(account: Account, replyingTo: Note?, quote: Note?) {
originalNote = replyingTo originalNote = replyingTo
replyingTo?.let { replyNote -> replyingTo?.let { replyNote ->
@ -199,6 +203,8 @@ open class NewPostViewModel : ViewModel() {
valueMinimum = null valueMinimum = null
consensusThreshold = null consensusThreshold = null
closedAt = null closedAt = null
wantsInvoice = false
} }
open fun findUrlInMessage(): String? { open fun findUrlInMessage(): String? {
@ -248,11 +254,15 @@ open class NewPostViewModel : ViewModel() {
} }
fun canPost(): Boolean { fun canPost(): Boolean {
return message.text.isNotBlank() && !isUploadingImage && return message.text.isNotBlank() && !isUploadingImage && !wantsInvoice &&
(!wantsPoll || pollOptions.values.all { it.isNotEmpty() }) (!wantsPoll || pollOptions.values.all { it.isNotEmpty() })
} }
fun canUsePoll(): Boolean { fun canUsePoll(): Boolean {
return originalNote?.event !is PrivateDmEvent && originalNote?.channel() == null return originalNote?.event !is PrivateDmEvent && originalNote?.channel() == null
} }
fun canAddLnInvoice(): Boolean {
return account?.userProfile()?.info?.lnAddress() != null
}
} }

View File

@ -1,7 +1,5 @@
package com.vitorpamplona.amethyst.ui.components package com.vitorpamplona.amethyst.ui.components
import android.content.Intent
import android.net.Uri
import android.widget.Toast import android.widget.Toast
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column 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.text.input.KeyboardType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat.startActivity
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @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 context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@ -73,7 +78,7 @@ fun InvoiceRequest(lud16: String, toUserPubKeyHex: String, account: Account, onC
) )
Text( Text(
text = stringResource(R.string.lightning_tips), text = titleText ?: stringResource(R.string.lightning_tips),
fontSize = 20.sp, fontSize = 20.sp,
fontWeight = FontWeight.W500, fontWeight = FontWeight.W500,
modifier = Modifier.padding(start = 10.dp) modifier = Modifier.padding(start = 10.dp)
@ -137,13 +142,7 @@ fun InvoiceRequest(lud16: String, toUserPubKeyHex: String, account: Account, onC
amount * 1000, amount * 1000,
message, message,
zapRequest?.toJson(), zapRequest?.toJson(),
onSuccess = { onSuccess = onSuccess,
runCatching {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$it"))
startActivity(context, intent, null)
}
onClose()
},
onError = { onError = {
scope.launch { scope.launch {
Toast.makeText(context, it, Toast.LENGTH_SHORT).show() 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 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)
} }
} }
} }

View File

@ -1,5 +1,7 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.* 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.ContentScale
import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.LocalUriHandler 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.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
@ -421,6 +425,7 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account, accountViewMode
val uri = LocalUriHandler.current val uri = LocalUriHandler.current
val clipboardManager = LocalClipboardManager.current val clipboardManager = LocalClipboardManager.current
val context = LocalContext.current
Row(verticalAlignment = Alignment.Bottom) { Row(verticalAlignment = Alignment.Bottom) {
user.bestDisplayName()?.let { user.bestDisplayName()?.let {
@ -555,9 +560,22 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account, accountViewMode
if (zapExpanded) { if (zapExpanded) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 5.dp)) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 5.dp)) {
InvoiceRequest(lud16, baseUser.pubkeyHex, account) { InvoiceRequest(lud16, baseUser.pubkeyHex, account,
zapExpanded = false 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
}
)
} }
} }
} }

View File

@ -288,4 +288,6 @@
<string name="custom_zaps_add_a_message">Add a public message</string> <string name="custom_zaps_add_a_message">Add a public message</string>
<string name="custom_zaps_add_a_message_example">Thank you for all your work!</string> <string name="custom_zaps_add_a_message_example">Thank you for all your work!</string>
<string name="lightning_create_and_add_invoice">Create and Add</string>
</resources> </resources>