From 5f459ac924284aebc0293abd5b1bb4e39350b10d Mon Sep 17 00:00:00 2001 From: Believethehype Date: Sun, 26 Mar 2023 23:57:25 +0200 Subject: [PATCH 1/3] Custom Messages for Zaps - Includes adding a custom message to sats in Backend Code - Added a simple window on double tap on the bolt symbol for custom amount + message - ! The popup might need some beautification - ! Messages are not shown yet in Amethyst - ! Window could provide options for non-zaps and private zaps in the future --- .../vitorpamplona/amethyst/model/Account.kt | 8 +- .../service/model/LnZapRequestEvent.kt | 6 +- .../amethyst/ui/components/InvoiceRequest.kt | 2 +- .../amethyst/ui/note/ReactionsRow.kt | 18 +- .../amethyst/ui/note/ZapOptionsDialog.kt | 180 ++++++++++++++++++ .../ui/screen/loggedIn/AccountViewModel.kt | 2 +- 6 files changed, 203 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapOptionsDialog.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 0e2c36ccd..7ca3323eb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -151,11 +151,11 @@ class Account( } } - fun createZapRequestFor(note: Note): LnZapRequestEvent? { + fun createZapRequestFor(note: Note, message: String = ""): LnZapRequestEvent? { if (!isWriteable()) return null note.event?.let { - return LnZapRequestEvent.create(it, userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } ?: localRelays.map { it.url }.toSet(), loggedIn.privKey!!) + return LnZapRequestEvent.create(it, userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } ?: localRelays.map { it.url }.toSet(), loggedIn.privKey!!, message) } return null @@ -179,10 +179,10 @@ class Account( return createZapRequestFor(user.pubkeyHex) } - fun createZapRequestFor(userPubKeyHex: String): LnZapRequestEvent? { + fun createZapRequestFor(userPubKeyHex: String, message: String = ""): LnZapRequestEvent? { if (!isWriteable()) return null - return LnZapRequestEvent.create(userPubKeyHex, userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } ?: localRelays.map { it.url }.toSet(), loggedIn.privKey!!) + return LnZapRequestEvent.create(userPubKeyHex, userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } ?: localRelays.map { it.url }.toSet(), loggedIn.privKey!!, message) } fun report(note: Note, type: ReportEvent.ReportType, content: String = "") { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt index 8863b7af4..23dc9d6b6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt @@ -23,9 +23,10 @@ class LnZapRequestEvent( originalNote: EventInterface, relays: Set, privateKey: ByteArray, + message: String, createdAt: Long = Date().time / 1000 ): LnZapRequestEvent { - val content = "" + val content = message val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() var tags = listOf( listOf("e", originalNote.id()), @@ -45,9 +46,10 @@ class LnZapRequestEvent( userHex: String, relays: Set, privateKey: ByteArray, + message: String, createdAt: Long = Date().time / 1000 ): LnZapRequestEvent { - val content = "" + val content = message val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = listOf( listOf("p", userHex), 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 8cf0b80c4..2fff33f83 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 @@ -130,7 +130,7 @@ fun InvoiceRequest(lud16: String, toUserPubKeyHex: String, account: Account, onC Button( modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp), onClick = { - val zapRequest = account.createZapRequestFor(toUserPubKeyHex) + val zapRequest = account.createZapRequestFor(toUserPubKeyHex, message) LightningAddressResolver().lnAddressInvoice( lud16, 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 b9b6be39b..e2e72ae25 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 @@ -303,10 +303,11 @@ fun ZapReaction( val zapsState by baseNote.live().zaps.observeAsState() val zappedNote = zapsState?.note + val zapMessage = "" var wantsToZap by remember { mutableStateOf(false) } var wantsToChangeZapAmount by remember { mutableStateOf(false) } - + var wantsToSetZapOptions by remember { mutableStateOf(false) } val grayTint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) val context = LocalContext.current val scope = rememberCoroutineScope() @@ -347,7 +348,7 @@ fun ZapReaction( accountViewModel.zap( baseNote, account.zapAmountChoices.first() * 1000, - "", + zapMessage, context, onError = { scope.launch { @@ -370,6 +371,9 @@ fun ZapReaction( }, onLongClick = { wantsToChangeZapAmount = true + }, + onDoubleClick = { + wantsToSetZapOptions = true } ) ) { @@ -402,6 +406,10 @@ fun ZapReaction( UpdateZapAmountDialog({ wantsToChangeZapAmount = false }, account = account) } + if (wantsToSetZapOptions) { + ZapOptionsDialog({ wantsToSetZapOptions = false }, account = account, accountViewModel, baseNote) + } + if (zappedNote?.isZappedBy(account.userProfile()) == true) { zappingProgress = 1f Icon( @@ -530,7 +538,7 @@ fun ZapAmountChoicePopup( val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account ?: return - + val zapMessage = "" val scope = rememberCoroutineScope() Popup( @@ -547,7 +555,7 @@ fun ZapAmountChoicePopup( accountViewModel.zap( baseNote, amountInSats * 1000, - "", + zapMessage, context, onError, onProgress @@ -571,7 +579,7 @@ fun ZapAmountChoicePopup( accountViewModel.zap( baseNote, amountInSats * 1000, - "", + zapMessage, context, onError, onProgress diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapOptionsDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapOptionsDialog.kt new file mode 100644 index 000000000..63af0de2f --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapOptionsDialog.kt @@ -0,0 +1,180 @@ +package com.vitorpamplona.amethyst.ui.note + +import android.widget.Toast +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.actions.CloseButton +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +class ZapOptionstViewModel : ViewModel() { + private var account: Account? = null + + var CustomAmount by mutableStateOf(TextFieldValue("")) + var CustomMessage by mutableStateOf(TextFieldValue("")) + + fun load(account: Account) { + this.account = account + } + + fun cancel() { + CustomAmount = TextFieldValue("") + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun ZapOptionsDialog(onClose: () -> Unit, account: Account, accountViewModel: AccountViewModel, baseNote: Note) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + val postViewModel: ZapOptionstViewModel = viewModel() + LaunchedEffect(account) { + postViewModel.load(account) + } + + Dialog( + onDismissRequest = { onClose() }, + properties = DialogProperties( + dismissOnClickOutside = false, + usePlatformDefaultWidth = false + ) + ) { + Surface() { + Column(modifier = Modifier.padding(10.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + CloseButton(onCancel = { + postViewModel.cancel() + onClose() + }) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 5.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedTextField( + // stringResource(R.string.new_amount_in_sats + label = { Text(text = "Custom Amount") }, + value = postViewModel.CustomAmount, + onValueChange = { + postViewModel.CustomAmount = it + }, + keyboardOptions = KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.None, + keyboardType = KeyboardType.Number + ), + placeholder = { + Text( + text = postViewModel.CustomAmount.text, + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + }, + singleLine = true, + modifier = Modifier + .padding(end = 10.dp) + .weight(1f) + ) + } + Spacer(modifier = Modifier.height(10.dp)) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedTextField( + // stringResource(R.string.new_amount_in_sats + label = { Text(text = "Message") }, + value = postViewModel.CustomMessage, + onValueChange = { + postViewModel.CustomMessage = it + }, + keyboardOptions = KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.None, + keyboardType = KeyboardType.Text + ), + placeholder = { + Text( + text = postViewModel.CustomMessage.text, + color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) + ) + }, + singleLine = true, + modifier = Modifier + .padding(end = 10.dp) + .weight(1f) + ) + + Button( + onClick = { + scope.launch(Dispatchers.IO) { + accountViewModel.zap( + baseNote, + postViewModel.CustomAmount.text.toLong() * 1000, + postViewModel.CustomMessage.text, + context, + onError = { + scope.launch { + // zappingProgress = 0f + Toast + .makeText(context, it, Toast.LENGTH_SHORT) + .show() + } + }, + onProgress = { + scope.launch(Dispatchers.Main) { + // zappingProgress = it + } + } + ) + } + onClose() + } + ) { + Text(text = "Zap", color = Color.White) + } + } + } + } + } +} 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 aee875037..c7a820706 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 @@ -60,7 +60,7 @@ class AccountViewModel(private val account: Account) : ViewModel() { return } - val zapRequest = account.createZapRequestFor(note) + val zapRequest = account.createZapRequestFor(note, message) onProgress(0.10f) From ad70cfb8d7e6a15209a6828ff4189ffbc426779c Mon Sep 17 00:00:00 2001 From: Believethehype Date: Tue, 28 Mar 2023 22:22:28 +0200 Subject: [PATCH 2/3] updated ZapCustomDialog --- .../amethyst/ui/note/ReactionsRow.kt | 8 +- ...ZapOptionsDialog.kt => ZapCustomDialog.kt} | 80 +++++++++++-------- 2 files changed, 51 insertions(+), 37 deletions(-) rename app/src/main/java/com/vitorpamplona/amethyst/ui/note/{ZapOptionsDialog.kt => ZapCustomDialog.kt} (71%) 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 e2e72ae25..991bdc668 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,7 @@ fun ZapReaction( var wantsToZap by remember { mutableStateOf(false) } var wantsToChangeZapAmount by remember { mutableStateOf(false) } - var wantsToSetZapOptions by remember { mutableStateOf(false) } + var wantsToSetCustomZap by remember { mutableStateOf(false) } val grayTint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) val context = LocalContext.current val scope = rememberCoroutineScope() @@ -373,7 +373,7 @@ fun ZapReaction( wantsToChangeZapAmount = true }, onDoubleClick = { - wantsToSetZapOptions = true + wantsToSetCustomZap = true } ) ) { @@ -406,8 +406,8 @@ fun ZapReaction( UpdateZapAmountDialog({ wantsToChangeZapAmount = false }, account = account) } - if (wantsToSetZapOptions) { - ZapOptionsDialog({ wantsToSetZapOptions = false }, account = account, accountViewModel, baseNote) + if (wantsToSetCustomZap) { + ZapCustomDialog({ wantsToSetCustomZap = false }, account = account, accountViewModel, baseNote) } if (zappedNote?.isZappedBy(account.userProfile()) == true) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapOptionsDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt similarity index 71% rename from app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapOptionsDialog.kt rename to app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt index 63af0de2f..e367b1ba4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapOptionsDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt @@ -1,20 +1,9 @@ package com.vitorpamplona.amethyst.ui.note import android.widget.Toast -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Button -import androidx.compose.material.MaterialTheme -import androidx.compose.material.OutlinedTextField -import androidx.compose.material.Surface -import androidx.compose.material.Text +import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -25,10 +14,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.ViewModel @@ -39,24 +31,29 @@ import com.vitorpamplona.amethyst.ui.actions.CloseButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch + class ZapOptionstViewModel : ViewModel() { private var account: Account? = null - var CustomAmount by mutableStateOf(TextFieldValue("")) - var CustomMessage by mutableStateOf(TextFieldValue("")) + var customAmount by mutableStateOf(TextFieldValue("1000")) + var customMessage by mutableStateOf(TextFieldValue("")) fun load(account: Account) { this.account = account } fun cancel() { - CustomAmount = TextFieldValue("") + customAmount = TextFieldValue("") } } +fun isNumeric(toCheck: String): Boolean { + return toCheck.toLongOrNull() != null +} + @OptIn(ExperimentalLayoutApi::class) @Composable -fun ZapOptionsDialog(onClose: () -> Unit, account: Account, accountViewModel: AccountViewModel, baseNote: Note) { +fun ZapCustomDialog(onClose: () -> Unit, account: Account, accountViewModel: AccountViewModel, baseNote: Note) { val context = LocalContext.current val scope = rememberCoroutineScope() val postViewModel: ZapOptionstViewModel = viewModel() @@ -84,6 +81,26 @@ fun ZapOptionsDialog(onClose: () -> Unit, account: Account, accountViewModel: Ac }) } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 5.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(com.vitorpamplona.amethyst.R.drawable.zap), + null, + modifier = Modifier.size(20.dp), + tint = Color.Unspecified + ) + Text( + text = "Custom Zap", + fontSize = 20.sp, + fontWeight = FontWeight.W500, + modifier = Modifier.padding(start = 10.dp) + ) + } Row( modifier = Modifier .fillMaxWidth() @@ -94,9 +111,11 @@ fun ZapOptionsDialog(onClose: () -> Unit, account: Account, accountViewModel: Ac OutlinedTextField( // stringResource(R.string.new_amount_in_sats label = { Text(text = "Custom Amount") }, - value = postViewModel.CustomAmount, + value = postViewModel.customAmount, onValueChange = { - postViewModel.CustomAmount = it + if (isNumeric(it.text)) { + postViewModel.customAmount = it + } }, keyboardOptions = KeyboardOptions.Default.copy( capitalization = KeyboardCapitalization.None, @@ -104,7 +123,7 @@ fun ZapOptionsDialog(onClose: () -> Unit, account: Account, accountViewModel: Ac ), placeholder = { Text( - text = postViewModel.CustomAmount.text, + text = postViewModel.customAmount.text, color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) }, @@ -125,9 +144,9 @@ fun ZapOptionsDialog(onClose: () -> Unit, account: Account, accountViewModel: Ac OutlinedTextField( // stringResource(R.string.new_amount_in_sats label = { Text(text = "Message") }, - value = postViewModel.CustomMessage, + value = postViewModel.customMessage, onValueChange = { - postViewModel.CustomMessage = it + postViewModel.customMessage = it }, keyboardOptions = KeyboardOptions.Default.copy( capitalization = KeyboardCapitalization.None, @@ -135,7 +154,7 @@ fun ZapOptionsDialog(onClose: () -> Unit, account: Account, accountViewModel: Ac ), placeholder = { Text( - text = postViewModel.CustomMessage.text, + text = postViewModel.customMessage.text, color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) }, @@ -150,20 +169,15 @@ fun ZapOptionsDialog(onClose: () -> Unit, account: Account, accountViewModel: Ac scope.launch(Dispatchers.IO) { accountViewModel.zap( baseNote, - postViewModel.CustomAmount.text.toLong() * 1000, - postViewModel.CustomMessage.text, + postViewModel.customAmount.text.toLong() * 1000, + postViewModel.customMessage.text, context, onError = { - scope.launch { - // zappingProgress = 0f - Toast - .makeText(context, it, Toast.LENGTH_SHORT) - .show() - } + Toast + .makeText(context, it, Toast.LENGTH_SHORT).show() }, onProgress = { scope.launch(Dispatchers.Main) { - // zappingProgress = it } } ) @@ -171,7 +185,7 @@ fun ZapOptionsDialog(onClose: () -> Unit, account: Account, accountViewModel: Ac onClose() } ) { - Text(text = "Zap", color = Color.White) + Text(text = "⚡Zap ", color = Color.White) } } } From 42e0cda2e2fee43d3f658eddeba645fcef9a5114 Mon Sep 17 00:00:00 2001 From: Believethehype Date: Tue, 28 Mar 2023 22:24:00 +0200 Subject: [PATCH 3/3] don't reset dialog on close --- .../java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt | 1 - 1 file changed, 1 deletion(-) 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 e367b1ba4..09c9e8e63 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 @@ -43,7 +43,6 @@ class ZapOptionstViewModel : ViewModel() { } fun cancel() { - customAmount = TextFieldValue("") } }