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 3b7ffa580..d91b65b47 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
@@ -150,6 +150,15 @@ class Account(
var transientHiddenUsers: ImmutableSet = persistentSetOf()
+ data class PaymentRequest(
+ val relayUrl: String,
+ val lnInvoice: String?,
+ val description: String?,
+ val otherOptionsUrl: String?
+ )
+ var transientPaymentRequestDismissals: Set = emptySet()
+ val transientPaymentRequests: MutableStateFlow> = MutableStateFlow(emptySet())
+
// Observers line up here.
val live: AccountLiveData = AccountLiveData(this)
val liveLanguages: AccountLiveData = AccountLiveData(this)
@@ -383,6 +392,21 @@ class Account(
}
}
+ fun addPaymentRequestIfNew(paymentRequest: PaymentRequest) {
+ if (!this.transientPaymentRequests.value.contains(paymentRequest) &&
+ !this.transientPaymentRequestDismissals.contains(paymentRequest)
+ ) {
+ this.transientPaymentRequests.value = transientPaymentRequests.value + paymentRequest
+ }
+ }
+
+ fun dismissPaymentRequest(request: PaymentRequest) {
+ if (this.transientPaymentRequests.value.contains(request)) {
+ this.transientPaymentRequests.value = transientPaymentRequests.value - request
+ this.transientPaymentRequestDismissals = transientPaymentRequestDismissals + request
+ }
+ }
+
var userProfileCache: User? = null
fun updateOptOutOptions(warnReports: Boolean, filterSpam: Boolean) {
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 b1adad63e..edd2ee124 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt
@@ -244,4 +244,12 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
}
}
}
+
+ override fun pay(relay: Relay, lnInvoice: String?, description: String?, otherOptionsUrl: String?) {
+ super.pay(relay, lnInvoice, description, otherOptionsUrl)
+
+ if (this::account.isInitialized) {
+ account.addPaymentRequestIfNew(Account.PaymentRequest(relay.url, lnInvoice, description, otherOptionsUrl))
+ }
+ }
}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt
index 284bbe185..5941293a8 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt
@@ -76,6 +76,15 @@ abstract class NostrDataSource(val debugName: String) {
override fun onAuth(relay: Relay, challenge: String) {
auth(relay, challenge)
}
+
+ override fun onPaymentRequired(
+ relay: Relay,
+ lnInvoice: String?,
+ description: String?,
+ otherOptionsUrl: String?
+ ) {
+ pay(relay, lnInvoice, description, otherOptionsUrl)
+ }
}
init {
@@ -190,4 +199,5 @@ abstract class NostrDataSource(val debugName: String) {
abstract fun updateChannelFilters()
open fun auth(relay: Relay, challenge: String) = Unit
+ open fun pay(relay: Relay, lnInvoice: String?, description: String?, otherOptionsUrl: String?) = Unit
}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt
index fa9dec7f8..9bfaebdf2 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt
@@ -115,14 +115,18 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
}
fun search(searchString: String) {
- println("DataSource: ${this.javaClass.simpleName} Search for $searchString")
- this.searchString = searchString
- invalidateFilters()
+ if (this.searchString != searchString) {
+ println("DataSource: ${this.javaClass.simpleName} Search for $searchString")
+ this.searchString = searchString
+ invalidateFilters()
+ }
}
fun clear() {
- println("DataSource: ${this.javaClass.simpleName} Clear")
- searchString = null
- invalidateFilters()
+ if (searchString != null) {
+ println("DataSource: ${this.javaClass.simpleName} Clear")
+ searchString = null
+ invalidateFilters()
+ }
}
}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt
index 9fa1804d6..c33a5c79b 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt
@@ -177,6 +177,14 @@ object Client : RelayPool.Listener {
}
}
+ override fun onPaymentRequired(relay: Relay, lnInvoice: String?, description: String?, otherOptionsUrl: String?) {
+ // Releases the Web thread for the new payload.
+ // May need to add a processing queue if processing new events become too costly.
+ GlobalScope.launch(Dispatchers.Default) {
+ listeners.forEach { it.onPaymentRequired(relay, lnInvoice, description, otherOptionsUrl) }
+ }
+ }
+
fun subscribe(listener: Listener) {
listeners = listeners.plus(listener)
}
@@ -215,5 +223,7 @@ object Client : RelayPool.Listener {
open fun onSendResponse(eventId: String, success: Boolean, message: String, relay: Relay) = Unit
open fun onAuth(relay: Relay, challenge: String) = Unit
+
+ open fun onPaymentRequired(relay: Relay, lnInvoice: String?, description: String?, otherOptionsUrl: String?) = Unit
}
}
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 426884cd8..8acf2cdc4 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
@@ -244,8 +244,12 @@ class Relay(
// Log.w("Relay", "Relay$url, ${msg[1].asString}")
it.onAuth(this@Relay, msgArray[1].asText())
}
+ "PAY" -> listeners.forEach {
+ // Log.w("Relay", "Relay$url, ${msg[1].asString}")
+ it.onPaymentRequired(this@Relay, msgArray[1].asText(), msgArray[2].asText(), msgArray[3].asText())
+ }
else -> listeners.forEach {
- // Log.w("Relay", "Relay something else $url, $channel")
+ Log.w("Relay", "Unsupported message: $newMessage")
it.onError(
this@Relay,
channel,
@@ -393,5 +397,10 @@ class Relay(
* @param type is 0 for disconnect and 1 for connect
*/
fun onRelayStateChange(relay: Relay, type: StateType, channel: String?)
+
+ /**
+ * Relay sent an invoice
+ */
+ fun onPaymentRequired(relay: Relay, lnInvoice: String?, description: String?, otherOptionsUrl: String?)
}
}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt
index 4c0d8e039..dcc08bc83 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt
@@ -112,6 +112,8 @@ object RelayPool : Relay.Listener {
fun onSendResponse(eventId: String, success: Boolean, message: String, relay: Relay)
fun onAuth(relay: Relay, challenge: String)
+
+ fun onPaymentRequired(relay: Relay, lnInvoice: String?, description: String?, otherOptionsUrl: String?)
}
override fun onEvent(relay: Relay, subscriptionId: String, event: Event) {
@@ -138,6 +140,10 @@ object RelayPool : Relay.Listener {
listeners.forEach { it.onAuth(relay, challenge) }
}
+ override fun onPaymentRequired(relay: Relay, lnInvoice: String?, description: String?, otherOptionsUrl: String?) {
+ listeners.forEach { it.onPaymentRequired(relay, lnInvoice, description, otherOptionsUrl) }
+ }
+
private fun updateStatus() {
val connected = connectedRelays()
val available = availableRelays()
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 5e7caa981..7671cc838 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
@@ -98,10 +98,6 @@ open class NewPostViewModel() : ViewModel() {
var canAddInvoice by mutableStateOf(false)
var wantsInvoice by mutableStateOf(false)
- data class ForwardZapSetup(val user: User) {
- var percentage by mutableStateOf(100)
- }
-
// Forward Zap to
var wantsForwardZapTo by mutableStateOf(false)
var forwardZapTo by mutableStateOf>(Split())
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/PayRequestDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/PayRequestDialog.kt
new file mode 100644
index 000000000..cb0fc32a5
--- /dev/null
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/PayRequestDialog.kt
@@ -0,0 +1,107 @@
+package com.vitorpamplona.amethyst.ui.actions
+
+import androidx.compose.animation.Crossfade
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.OpenInNew
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonColors
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.platform.LocalUriHandler
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextDirection
+import com.vitorpamplona.amethyst.R
+import com.vitorpamplona.amethyst.ui.components.InvoicePreview
+import com.vitorpamplona.amethyst.ui.components.LoadValueFromInvoice
+import com.vitorpamplona.amethyst.ui.theme.Size16dp
+import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
+import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
+
+@Composable
+fun PayRequestDialog(
+ title: String,
+ textContent: String,
+ lnInvoice: String?,
+ textContent2: String,
+ otherOptions: String?,
+ buttonColors: ButtonColors = ButtonDefaults.buttonColors(),
+ onDismiss: () -> Unit
+) {
+ val uri = LocalUriHandler.current
+
+ val uriOpener: @Composable (() -> Unit) = otherOptions?.let {
+ {
+ Button(
+ onClick = {
+ runCatching {
+ uri.openUri(it)
+ }
+ },
+ colors = buttonColors,
+ contentPadding = PaddingValues(horizontal = Size16dp)
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.OpenInNew,
+ contentDescription = null
+ )
+ Spacer(StdHorzSpacer)
+ Text(stringResource(R.string.other_options))
+ }
+ }
+ }
+ } ?: {
+ Row() {}
+ }
+
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ title = {
+ Text(title)
+ },
+ text = {
+ Column {
+ Text(textContent)
+ Spacer(modifier = StdVertSpacer)
+ if (lnInvoice != null) {
+ LoadValueFromInvoice(lnbcWord = lnInvoice) { invoiceAmount ->
+ Crossfade(targetState = invoiceAmount, label = "PayRequestDialog") {
+ if (it != null) {
+ InvoicePreview(it.invoice, it.amount)
+ } else {
+ Text(
+ text = lnInvoice,
+ style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
+ )
+ }
+ }
+ }
+ }
+ Spacer(modifier = StdVertSpacer)
+ Text(textContent2)
+ }
+ },
+ confirmButton = uriOpener,
+ dismissButton = {
+ TextButton(
+ onClick = {
+ onDismiss()
+ }
+ ) {
+ Text(text = stringResource(R.string.dismiss))
+ }
+ }
+ )
+}
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt
index 4f090867b..a72f1568b 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoicePreview.kt
@@ -16,6 +16,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -41,9 +42,12 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.text.NumberFormat
+@Stable
+data class InvoiceAmount(val invoice: String, val amount: String?)
+
@Composable
-fun MayBeInvoicePreview(lnbcWord: String) {
- var lnInvoice by remember { mutableStateOf?>(null) }
+fun LoadValueFromInvoice(lnbcWord: String, inner: @Composable (invoiceAmount: InvoiceAmount?) -> Unit) {
+ var lnInvoice by remember { mutableStateOf(null) }
LaunchedEffect(key1 = lnbcWord) {
launch(Dispatchers.IO) {
@@ -56,19 +60,26 @@ fun MayBeInvoicePreview(lnbcWord: String) {
null
}
- lnInvoice = Pair(myInvoice, myInvoiceAmount)
+ lnInvoice = InvoiceAmount(myInvoice, myInvoiceAmount)
}
}
}
- Crossfade(targetState = lnInvoice) {
- if (it != null) {
- InvoicePreview(it.first, it.second)
- } else {
- Text(
- text = lnbcWord,
- style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
- )
+ inner(lnInvoice)
+}
+
+@Composable
+fun MayBeInvoicePreview(lnbcWord: String) {
+ LoadValueFromInvoice(lnbcWord = lnbcWord) { invoiceAmount ->
+ Crossfade(targetState = invoiceAmount, label = "MayBeInvoicePreview") {
+ if (it != null) {
+ InvoicePreview(it.invoice, it.amount)
+ } else {
+ Text(
+ text = lnbcWord,
+ style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
+ )
+ }
}
}
}
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 c295d30fe..bcf1cb2a2 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
@@ -952,6 +952,12 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
)
}
}
+
+ fun dismissPaymentRequest(request: Account.PaymentRequest) {
+ viewModelScope.launch(Dispatchers.IO) {
+ account.dismissPaymentRequest(request)
+ }
+ }
}
class HasNotificationDot(bottomNavigationItems: ImmutableList) {
diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt
index 41e6cba07..8ca25403d 100644
--- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt
+++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt
@@ -44,6 +44,7 @@ import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
@@ -52,8 +53,10 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavBackStackEntry
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
+import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.BooleanType
import com.vitorpamplona.amethyst.ui.actions.InformationDialog
+import com.vitorpamplona.amethyst.ui.actions.PayRequestDialog
import com.vitorpamplona.amethyst.ui.buttons.ChannelFabColumn
import com.vitorpamplona.amethyst.ui.buttons.NewCommunityNoteButton
import com.vitorpamplona.amethyst.ui.buttons.NewImageButton
@@ -133,6 +136,7 @@ fun MainScreen(
}
DisplayErrorMessages(accountViewModel)
+ DisplayPayMessages(accountViewModel)
val navPopBack = remember(navController) {
{
@@ -416,6 +420,25 @@ private fun DisplayErrorMessages(accountViewModel: AccountViewModel) {
}
}
+@Composable
+private fun DisplayPayMessages(accountViewModel: AccountViewModel) {
+ val openDialogMsg = accountViewModel.account.transientPaymentRequests.collectAsStateWithLifecycle(null)
+
+ openDialogMsg.value?.firstOrNull()?.let { request ->
+ PayRequestDialog(
+ stringResource(id = R.string.payment_required_title, request.relayUrl.removePrefix("wss://").removeSuffix("/")),
+ request.description?.let {
+ stringResource(id = R.string.payment_required_explain, it)
+ } ?: stringResource(id = R.string.payment_required_explain_null_description),
+ request.lnInvoice,
+ stringResource(id = R.string.payment_required_explain2),
+ request.otherOptionsUrl
+ ) {
+ accountViewModel.dismissPaymentRequest(request)
+ }
+ }
+}
+
@Composable
fun WatchNavStateToUpdateBarVisibility(navState: State, onReset: () -> Unit) {
LaunchedEffect(key1 = navState.value) {
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 6e373593b..150003030 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -661,4 +661,11 @@
To receive push notifications, install any app that supports [Unified Push](https://unifiedpush.org/), such as [Nfty](https://ntfy.sh/).
After installing, select the app you want to use in the Settings.
+
+ Payment Required for %1$s
+ Relay has requested a payment of the invoice below for the %1$s.
+ Relay has requested a payment of the invoice below
+ If you do not intent to use this relay anymore, please remove it from your relay list
+ Dismiss
+ See other options