Refactoring Zap Error message screen to allow sending messages directly to each receiver with an error

This commit is contained in:
Vitor Pamplona 2024-12-31 15:51:31 -05:00
parent 497ae937fd
commit 160d4722c0
12 changed files with 540 additions and 219 deletions

View File

@ -72,8 +72,8 @@ suspend inline fun <T> tryAndWait(
suspend fun <T, K> collectSuccessfulOperations( suspend fun <T, K> collectSuccessfulOperations(
items: List<T>, items: List<T>,
runRequestFor: (T, (K) -> Unit) -> Unit, runRequestFor: (T, (K) -> Unit) -> Unit,
output: MutableMap<T, K> = mutableMapOf(), output: MutableList<K> = mutableListOf(),
onReady: suspend (MutableMap<T, K>) -> Unit, onReady: suspend (List<K>) -> Unit,
) { ) {
if (items.isEmpty()) { if (items.isEmpty()) {
onReady(output) onReady(output)
@ -87,7 +87,7 @@ suspend fun <T, K> collectSuccessfulOperations(
} }
if (result != null) { if (result != null) {
output[it] = result output.add(result)
} }
} }

View File

@ -28,6 +28,7 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource.user
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
@ -61,7 +62,7 @@ class ZapPaymentHandler(
context: Context, context: Context,
showErrorIfNoLnAddress: Boolean, showErrorIfNoLnAddress: Boolean,
forceProxy: (String) -> Boolean, forceProxy: (String) -> Boolean,
onError: (String, String) -> Unit, onError: (String, String, User?) -> Unit,
onProgress: (percent: Float) -> Unit, onProgress: (percent: Float) -> Unit,
onPayViaIntent: (ImmutableList<Payable>) -> Unit, onPayViaIntent: (ImmutableList<Payable>) -> Unit,
zapType: LnZapEvent.ZapType, zapType: LnZapEvent.ZapType,
@ -89,6 +90,7 @@ class ZapPaymentHandler(
context, context,
R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats, R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats,
), ),
note.author,
) )
} }
return@withContext return@withContext
@ -107,6 +109,7 @@ class ZapPaymentHandler(
context, context,
R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats, R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats,
), ),
note.author,
) )
} }
return@withContext return@withContext
@ -124,10 +127,10 @@ class ZapPaymentHandler(
onProgress(0.05f) onProgress(0.05f)
} }
assembleAllInvoices(splitZapRequestPairs.toList(), amountMilliSats, message, showErrorIfNoLnAddress, forceProxy, onError, onProgress = { assembleAllInvoices(splitZapRequestPairs, amountMilliSats, message, showErrorIfNoLnAddress, forceProxy, onError, onProgress = {
onProgress(it * 0.7f + 0.05f) // keeps within range. onProgress(it * 0.7f + 0.05f) // keeps within range.
}, context) { }, context) { payables ->
if (it.isEmpty()) { if (payables.isEmpty()) {
onProgress(0.00f) onProgress(0.00f)
return@assembleAllInvoices return@assembleAllInvoices
} else { } else {
@ -135,22 +138,14 @@ class ZapPaymentHandler(
} }
if (account.hasWalletConnectSetup()) { if (account.hasWalletConnectSetup()) {
payViaNWC(it.values.map { it.invoice }, note, onError, onProgress = { payViaNWC(payables, note, onError = onError, onProgress = {
onProgress(it * 0.25f + 0.75f) // keeps within range. onProgress(it * 0.25f + 0.75f) // keeps within range.
}, context) { }, context) {
// onProgress(1f) // onProgress(1f)
} }
} else { } else {
onPayViaIntent( onPayViaIntent(
it payables.toImmutableList(),
.map {
Payable(
info = it.key.first,
user = it.key.second.user,
amountMilliSats = it.value.zapValue,
invoice = it.value.invoice,
)
}.toImmutableList(),
) )
onProgress(0f) onProgress(0f)
@ -169,7 +164,8 @@ class ZapPaymentHandler(
return roundedZapValue return roundedZapValue
} }
class SignAllZapRequestsReturn( class ZapRequestReady(
val inputSetup: ZapSplitSetup,
val zapRequestJson: String?, val zapRequestJson: String?,
val user: User? = null, val user: User? = null,
) )
@ -180,7 +176,7 @@ class ZapPaymentHandler(
message: String, message: String,
zapType: LnZapEvent.ZapType, zapType: LnZapEvent.ZapType,
zapsToSend: List<ZapSplitSetup>, zapsToSend: List<ZapSplitSetup>,
onAllDone: suspend (MutableMap<ZapSplitSetup, SignAllZapRequestsReturn>) -> Unit, onAllDone: suspend (List<ZapRequestReady>) -> Unit,
) { ) {
val authorRelayList = val authorRelayList =
note.author note.author
@ -194,13 +190,13 @@ class ZapPaymentHandler(
)?.readRelays() )?.readRelays()
}?.toSet() }?.toSet()
collectSuccessfulOperations<ZapSplitSetup, SignAllZapRequestsReturn>( collectSuccessfulOperations<ZapSplitSetup, ZapRequestReady>(
items = zapsToSend, items = zapsToSend,
runRequestFor = { next: ZapSplitSetup, onReady -> runRequestFor = { next: ZapSplitSetup, onReady ->
if (next.isLnAddress) { if (next.isLnAddress) {
prepareZapRequestIfNeeded(note, pollOption, message, zapType) { zapRequestJson -> prepareZapRequestIfNeeded(note, pollOption, message, zapType) { zapRequestJson ->
if (zapRequestJson != null) { if (zapRequestJson != null) {
onReady(SignAllZapRequestsReturn(zapRequestJson)) onReady(ZapRequestReady(next, zapRequestJson))
} }
} }
} else { } else {
@ -216,7 +212,7 @@ class ZapPaymentHandler(
) + (authorRelayList ?: emptySet()) ) + (authorRelayList ?: emptySet())
prepareZapRequestIfNeeded(note, pollOption, message, zapType, user, userRelayList) { zapRequestJson -> prepareZapRequestIfNeeded(note, pollOption, message, zapType, user, userRelayList) { zapRequestJson ->
onReady(SignAllZapRequestsReturn(zapRequestJson, user)) onReady(ZapRequestReady(next, zapRequestJson, user))
} }
} }
}, },
@ -225,32 +221,33 @@ class ZapPaymentHandler(
} }
suspend fun assembleAllInvoices( suspend fun assembleAllInvoices(
invoices: List<Pair<ZapSplitSetup, SignAllZapRequestsReturn>>, requests: List<ZapRequestReady>,
totalAmountMilliSats: Long, totalAmountMilliSats: Long,
message: String, message: String,
showErrorIfNoLnAddress: Boolean, showErrorIfNoLnAddress: Boolean,
forceProxy: (String) -> Boolean, forceProxy: (String) -> Boolean,
onError: (String, String) -> Unit, onError: (String, String, User?) -> Unit,
onProgress: (percent: Float) -> Unit, onProgress: (percent: Float) -> Unit,
context: Context, context: Context,
onAllDone: suspend (MutableMap<Pair<ZapSplitSetup, SignAllZapRequestsReturn>, AssembleInvoiceReturn>) -> Unit, onAllDone: suspend (List<Payable>) -> Unit,
) { ) {
var progressAllPayments = 0.00f var progressAllPayments = 0.00f
val totalWeight = invoices.sumOf { it.first.weight } val totalWeight = requests.sumOf { it.inputSetup.weight }
collectSuccessfulOperations<Pair<ZapSplitSetup, SignAllZapRequestsReturn>, AssembleInvoiceReturn>( collectSuccessfulOperations<ZapRequestReady, Payable>(
items = invoices, items = requests,
runRequestFor = { splitZapRequestPair: Pair<ZapSplitSetup, SignAllZapRequestsReturn>, onReady -> runRequestFor = { splitZapRequestPair: ZapRequestReady, onReady ->
assembleInvoice( assembleInvoice(
splitSetup = splitZapRequestPair.first, splitSetup = splitZapRequestPair.inputSetup,
nostrZapRequest = splitZapRequestPair.second.zapRequestJson, nostrZapRequest = splitZapRequestPair.zapRequestJson,
zapValue = calculateZapValue(totalAmountMilliSats, splitZapRequestPair.first.weight, totalWeight), toUser = splitZapRequestPair.user,
zapValue = calculateZapValue(totalAmountMilliSats, splitZapRequestPair.inputSetup.weight, totalWeight),
message = message, message = message,
showErrorIfNoLnAddress = showErrorIfNoLnAddress, showErrorIfNoLnAddress = showErrorIfNoLnAddress,
forceProxy = forceProxy, forceProxy = forceProxy,
onError = onError, onError = onError,
onProgressStep = { percentStepForThisPayment -> onProgressStep = { percentStepForThisPayment ->
progressAllPayments += percentStepForThisPayment / invoices.size progressAllPayments += percentStepForThisPayment / requests.size
onProgress(progressAllPayments) onProgress(progressAllPayments)
}, },
context = context, context = context,
@ -261,30 +258,35 @@ class ZapPaymentHandler(
) )
} }
class Paid(
payable: Payable,
success: Boolean,
)
suspend fun payViaNWC( suspend fun payViaNWC(
invoices: List<String>, payables: List<Payable>,
note: Note, note: Note,
onError: (String, String) -> Unit, onError: (String, String, User?) -> Unit,
onProgress: (percent: Float) -> Unit, onProgress: (percent: Float) -> Unit,
context: Context, context: Context,
onAllDone: suspend (MutableMap<String, Boolean>) -> Unit, onAllDone: suspend (List<Paid>) -> Unit,
) { ) {
var progressAllPayments = 0.00f var progressAllPayments = 0.00f
collectSuccessfulOperations<String, Boolean>( collectSuccessfulOperations<Payable, Paid>(
items = invoices, items = payables,
runRequestFor = { invoice: String, onReady -> runRequestFor = { payable: Payable, onReady ->
account.sendZapPaymentRequestFor( account.sendZapPaymentRequestFor(
bolt11 = invoice, bolt11 = payable.invoice,
zappedNote = note, zappedNote = note,
onSent = { onSent = {
progressAllPayments += 0.5f / invoices.size progressAllPayments += 0.5f / payables.size
onProgress(progressAllPayments) onProgress(progressAllPayments)
onReady(true) onReady(Paid(payable, true))
}, },
onResponse = { response -> onResponse = { response ->
if (response is PayInvoiceErrorResponse) { if (response is PayInvoiceErrorResponse) {
progressAllPayments += 0.5f / invoices.size progressAllPayments += 0.5f / payables.size
onProgress(progressAllPayments) onProgress(progressAllPayments)
onError( onError(
stringRes(context, R.string.error_dialog_pay_invoice_error), stringRes(context, R.string.error_dialog_pay_invoice_error),
@ -294,9 +296,10 @@ class ZapPaymentHandler(
response.error?.message response.error?.message
?: response.error?.code?.toString() ?: "Error parsing error message", ?: response.error?.code?.toString() ?: "Error parsing error message",
), ),
payable.user,
) )
} else { } else {
progressAllPayments += 0.5f / invoices.size progressAllPayments += 0.5f / payables.size
onProgress(progressAllPayments) onProgress(progressAllPayments)
} }
}, },
@ -306,32 +309,26 @@ class ZapPaymentHandler(
) )
} }
class AssembleInvoiceReturn(
val zapValue: Long,
val invoice: String,
)
private fun assembleInvoice( private fun assembleInvoice(
splitSetup: ZapSplitSetup, splitSetup: ZapSplitSetup,
nostrZapRequest: String?, nostrZapRequest: String?,
toUser: User?,
zapValue: Long, zapValue: Long,
message: String, message: String,
showErrorIfNoLnAddress: Boolean = true, showErrorIfNoLnAddress: Boolean = true,
forceProxy: (String) -> Boolean, forceProxy: (String) -> Boolean,
onError: (String, String) -> Unit, onError: (String, String, User?) -> Unit,
onProgressStep: (percent: Float) -> Unit, onProgressStep: (percent: Float) -> Unit,
context: Context, context: Context,
onReady: (AssembleInvoiceReturn) -> Unit, onReady: (Payable) -> Unit,
) { ) {
var progressThisPayment = 0.00f var progressThisPayment = 0.00f
var user: User? = null
val lud16 = val lud16 =
if (splitSetup.isLnAddress) { if (splitSetup.isLnAddress) {
splitSetup.lnAddressOrPubKeyHex splitSetup.lnAddressOrPubKeyHex
} else { } else {
user = LocalCache.getUserIfExists(splitSetup.lnAddressOrPubKeyHex) toUser?.info?.lnAddress()
user?.info?.lnAddress()
} }
if (lud16 != null) { if (lud16 != null) {
@ -342,7 +339,9 @@ class ZapPaymentHandler(
message = message, message = message,
nostrRequest = nostrZapRequest, nostrRequest = nostrZapRequest,
forceProxy = forceProxy, forceProxy = forceProxy,
onError = onError, onError = { title, msg ->
onError(title, msg, toUser)
},
onProgress = { onProgress = {
val step = it - progressThisPayment val step = it - progressThisPayment
progressThisPayment = it progressThisPayment = it
@ -351,7 +350,14 @@ class ZapPaymentHandler(
context = context, context = context,
onSuccess = { onSuccess = {
onProgressStep(1 - progressThisPayment) onProgressStep(1 - progressThisPayment)
onReady(AssembleInvoiceReturn(zapValue, it)) onReady(
Payable(
info = splitSetup,
user = toUser,
amountMilliSats = zapValue,
invoice = it,
),
)
}, },
) )
} else { } else {
@ -366,6 +372,7 @@ class ZapPaymentHandler(
R.string.user_x_does_not_have_a_lightning_address_setup_to_receive_sats, R.string.user_x_does_not_have_a_lightning_address_setup_to_receive_sats,
user?.toBestDisplayName() ?: splitSetup.lnAddressOrPubKeyHex, user?.toBestDisplayName() ?: splitSetup.lnAddressOrPubKeyHex,
), ),
null,
) )
} }
} }

View File

@ -24,12 +24,14 @@ import android.content.Context
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.HttpStatusMessages
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
import com.vitorpamplona.quartz.encoders.Lud06 import com.vitorpamplona.quartz.encoders.Lud06
import okhttp3.Request import okhttp3.Request
import okhttp3.Response
import java.math.BigDecimal import java.math.BigDecimal
import java.math.RoundingMode import java.math.RoundingMode
import java.net.URLEncoder import java.net.URLEncoder
@ -95,7 +97,7 @@ class LightningAddressResolver {
.the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct, .the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct,
url, url,
lnaddress, lnaddress,
it.code.toString(), errorMessage(it, context),
), ),
) )
} }
@ -154,12 +156,36 @@ class LightningAddressResolver {
} else { } else {
onError( onError(
stringRes(context, R.string.error_unable_to_fetch_invoice), stringRes(context, R.string.error_unable_to_fetch_invoice),
stringRes(context, R.string.could_not_fetch_invoice_from, urlBinder), stringRes(context, R.string.could_not_fetch_invoice_from_details, lnCallback, errorMessage(it, context)),
) )
} }
} }
} }
fun errorMessage(
response: Response,
context: Context,
): String {
val errorMessage =
runCatching {
jacksonObjectMapper().readTree(response.body.string())
}.getOrNull()?.let { tree ->
val status = tree.get("status")?.asText()
val message = tree.get("message")?.asText()
if (status == "error" && message != null) {
message
} else {
tree.get("error")?.get("message")?.asText()
}
}
return errorMessage
?: HttpStatusMessages.resourceIdFor(response.code)?.let { stringRes(context, it) }
?: response.message.ifBlank { null }
?: response.code.toString()
}
fun lnAddressInvoice( fun lnAddressInvoice(
lnaddress: String, lnaddress: String,
milliSats: Long, milliSats: Long,
@ -195,7 +221,7 @@ class LightningAddressResolver {
null null
} }
val callback = lnurlp?.get("callback")?.asText() val callback = lnurlp?.get("callback")?.asText()?.ifBlank { null }
if (callback == null) { if (callback == null) {
onError( onError(

View File

@ -0,0 +1,110 @@
/**
* 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.ui.note
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Done
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.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size16dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
@Composable
@Preview
fun ErrorMessageContentPreview() {
ThemeComparisonColumn {
ErrorMessageDialog(
title = "Title",
textContent = "This is an Error Message",
onClickStartMessage = { },
onDismiss = { },
)
}
}
@Composable
fun ErrorMessageDialog(
title: String,
textContent: String,
buttonColors: ButtonColors = ButtonDefaults.buttonColors(),
onClickStartMessage: (() -> Unit)? = null,
onDismiss: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(title) },
text = { SelectionContainer { Text(textContent) } },
confirmButton = {
Row(
modifier = Modifier.padding(vertical = 8.dp).fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
onClickStartMessage?.let {
TextButton(onClick = onClickStartMessage) {
Icon(
painter = painterResource(R.drawable.ic_dm),
contentDescription = null,
)
Spacer(StdHorzSpacer)
Text(stringRes(R.string.error_dialog_talk_to_user))
}
}
Button(
onClick = onDismiss,
colors = buttonColors,
contentPadding = PaddingValues(horizontal = Size16dp),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Outlined.Done,
contentDescription = null,
)
Spacer(StdHorzSpacer)
Text(stringRes(R.string.error_dialog_button_ok))
}
}
}
},
)
}

View File

@ -0,0 +1,241 @@
/**
* 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.ui.note
import androidx.compose.foundation.layout.Arrangement
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.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Done
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource.user
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.Size16dp
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.Size30Modifier
import com.vitorpamplona.amethyst.ui.theme.Size30dp
import com.vitorpamplona.amethyst.ui.theme.Size40dp
import com.vitorpamplona.amethyst.ui.theme.Size5dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
@Composable
@Preview
fun MultiUserErrorMessageContentPreview() {
val accountViewModel = mockAccountViewModel()
val nav = EmptyNav
var user1: User? = null
var user2: User? = null
var user3: User? = null
runBlocking {
withContext(Dispatchers.IO) {
user1 = LocalCache.getOrCreateUser("aabbccaabbccaabbcc")
user2 = LocalCache.getOrCreateUser("bbbccabbbccabbbcca")
user3 = LocalCache.getOrCreateUser("ccaadaccaadaccaada")
}
}
val model: UserBasedErrorMessageViewModel = viewModel()
model.add("Could not fetch invoice from https://minibits.cash/.well-known/lnurlp/victorieeman: There are too many unpaid invoices for this name.", user1)
model.add("Could not fetch invoice from https://minibits.cash/.well-known/lnurlp/victorieeman: There are too many unpaid invoices for this name.", user2)
model.add("Could not fetch invoice from https://minibits.cash/.well-known/lnurlp/victorieeman: There are too many unpaid invoices for this name.", user3)
ThemeComparisonColumn {
MultiUserErrorMessageDialogInner(
title = "Couldn't not zap",
model = model,
accountViewModel = accountViewModel,
nav = nav,
)
}
}
@Stable
class UserBasedErrorMessageViewModel : ViewModel() {
val errors = MutableStateFlow<List<UserBasedErrorMessage>>(emptyList())
val hasErrors = errors.map { it.isNotEmpty() }
fun add(
message: String,
user: User?,
) {
add(UserBasedErrorMessage(message, user))
}
fun add(newError: UserBasedErrorMessage) {
errors.update {
it + newError
}
}
fun clearErrors() {
errors.update {
emptyList()
}
}
}
class UserBasedErrorMessage(
val error: String,
val user: User?,
)
@Composable
fun MultiUserErrorMessageDialog(
title: String,
model: UserBasedErrorMessageViewModel,
accountViewModel: AccountViewModel,
nav: INav,
) {
val hasErrors by model.hasErrors.collectAsStateWithLifecycle(false)
if (hasErrors) {
MultiUserErrorMessageDialogInner(title, model, accountViewModel, nav)
}
}
@Composable
fun MultiUserErrorMessageDialogInner(
title: String,
model: UserBasedErrorMessageViewModel,
accountViewModel: AccountViewModel,
nav: INav,
) {
AlertDialog(
onDismissRequest = model::clearErrors,
title = { Text(title) },
text = {
val errorState by model.errors.collectAsStateWithLifecycle(emptyList())
LazyColumn {
itemsIndexed(errorState) { index, it ->
ErrorRow(it, accountViewModel, nav)
if (index < errorState.size - 1) {
HorizontalDivider(thickness = DividerThickness)
}
}
}
},
confirmButton = {
Button(
onClick = model::clearErrors,
contentPadding = PaddingValues(horizontal = Size16dp),
) {
Icon(
imageVector = Icons.Outlined.Done,
contentDescription = null,
)
Spacer(StdHorzSpacer)
Text(stringRes(R.string.error_dialog_button_ok))
}
},
)
}
@Composable
fun ErrorRow(
errorState: UserBasedErrorMessage,
accountViewModel: AccountViewModel,
nav: INav,
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
errorState.user?.let {
val scope = rememberCoroutineScope()
Column(Modifier.width(Size40dp), horizontalAlignment = Alignment.Start) {
// Box(Size30Modifier.background(Color.Red))
UserPicture(errorState.user, Size30dp, Modifier, accountViewModel, nav)
Spacer(StdVertSpacer)
IconButton(
modifier = Size30Modifier,
onClick = {
scope.launch(Dispatchers.IO) {
nav.nav(routeToMessage(it, errorState.error, accountViewModel))
}
},
) {
val descriptor =
it.info?.bestName()?.let {
stringRes(R.string.error_dialog_talk_to_user_name, it)
} ?: stringRes(R.string.error_dialog_talk_to_user)
Icon(
painter = painterResource(R.drawable.ic_dm),
contentDescription = descriptor,
modifier = Size20Modifier,
tint = MaterialTheme.colorScheme.primary,
)
}
}
}
Row(Modifier.padding(top = Size5dp).weight(1f)) {
SelectionContainer {
Text(errorState.error)
}
}
}
}

View File

@ -73,6 +73,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.service.ZapPaymentHandler
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
@ -559,7 +560,7 @@ fun ZapVote(
poolOption.option, poolOption.option,
"", "",
context, context,
onError = { title, message -> onError = { title, message, user ->
zappingProgress = 0f zappingProgress = 0f
showErrorMessageDialog = StringToastMsg(title, message) showErrorMessageDialog = StringToastMsg(title, message)
}, },
@ -583,7 +584,7 @@ fun ZapVote(
zappingProgress = 0f zappingProgress = 0f
}, },
onChangeAmount = { wantsToZap = false }, onChangeAmount = { wantsToZap = false },
onError = { title, message -> onError = { title, message, user ->
showErrorMessageDialog = StringToastMsg(title, message) showErrorMessageDialog = StringToastMsg(title, message)
zappingProgress = 0f zappingProgress = 0f
}, },
@ -604,7 +605,7 @@ fun ZapVote(
showErrorMessageDialog = showErrorMessageDialog =
StringToastMsg( StringToastMsg(
stringRes(context, R.string.error_dialog_zap_error), stringRes(context, R.string.error_dialog_zap_error),
it, it.error,
) )
} }
}, },
@ -613,7 +614,7 @@ fun ZapVote(
showErrorMessageDialog = showErrorMessageDialog =
StringToastMsg( StringToastMsg(
stringRes(context, R.string.error_dialog_zap_error), stringRes(context, R.string.error_dialog_zap_error),
it, it.error,
) )
} }
}, },
@ -681,7 +682,7 @@ fun FilteredZapAmountChoicePopup(
pollOption: Int, pollOption: Int,
onDismiss: () -> Unit, onDismiss: () -> Unit,
onChangeAmount: () -> Unit, onChangeAmount: () -> Unit,
onError: (title: String, text: String) -> Unit, onError: (title: String, text: String, toUser: User?) -> Unit,
onProgress: (percent: Float) -> Unit, onProgress: (percent: Float) -> Unit,
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit, onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
) { ) {

View File

@ -98,9 +98,12 @@ import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map import androidx.lifecycle.map
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource.user
import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.service.ZapPaymentHandler
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.components.ClickableBox
@ -108,7 +111,6 @@ import com.vitorpamplona.amethyst.ui.components.GenericLoadable
import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.buildNewPostRoute import com.vitorpamplona.amethyst.ui.navigation.buildNewPostRoute
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
import com.vitorpamplona.amethyst.ui.note.types.EditState import com.vitorpamplona.amethyst.ui.note.types.EditState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
@ -996,7 +998,7 @@ fun ZapReaction(
var wantsToZap by remember { mutableStateOf(false) } var wantsToZap by remember { mutableStateOf(false) }
var wantsToChangeZapAmount by remember { mutableStateOf(false) } var wantsToChangeZapAmount by remember { mutableStateOf(false) }
var wantsToSetCustomZap by remember { mutableStateOf(false) } var wantsToSetCustomZap by remember { mutableStateOf(false) }
var showErrorMessageDialog by remember { mutableStateOf<List<String>>(emptyList()) } val errorViewModel: UserBasedErrorMessageViewModel = viewModel()
var wantsToPay by var wantsToPay by
remember(baseNote) { remember(baseNote) {
mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>( mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>(
@ -1028,10 +1030,10 @@ fun ZapReaction(
wantsToZap = true wantsToZap = true
} }
}, },
onError = { _, message -> onError = { _, message, user ->
scope.launch { scope.launch {
zappingProgress = 0f zappingProgress = 0f
showErrorMessageDialog = showErrorMessageDialog + message errorViewModel.add(message, user)
} }
}, },
onPayViaIntent = { wantsToPay = it }, onPayViaIntent = { wantsToPay = it },
@ -1057,10 +1059,10 @@ fun ZapReaction(
wantsToChangeZapAmount = true wantsToChangeZapAmount = true
} }
}, },
onError = { _, message -> onError = { _, message, user ->
scope.launch { scope.launch {
zappingProgress = 0f zappingProgress = 0f
showErrorMessageDialog = showErrorMessageDialog + message errorViewModel.add(message, user)
} }
}, },
onProgress = { scope.launch(Dispatchers.Main) { zappingProgress = it } }, onProgress = { scope.launch(Dispatchers.Main) { zappingProgress = it } },
@ -1068,22 +1070,12 @@ fun ZapReaction(
) )
} }
if (showErrorMessageDialog.isNotEmpty()) { MultiUserErrorMessageDialog(
val msg = showErrorMessageDialog.joinToString("\n") title = stringRes(id = R.string.error_dialog_zap_error),
ErrorMessageDialog( model = errorViewModel,
title = stringRes(id = R.string.error_dialog_zap_error), accountViewModel = accountViewModel,
textContent = msg, nav = nav,
onClickStartMessage = { )
baseNote.author?.let {
scope.launch(Dispatchers.IO) {
val route = routeToMessage(it, msg, accountViewModel)
nav.nav(route)
}
}
},
onDismiss = { showErrorMessageDialog = emptyList() },
)
}
if (wantsToChangeZapAmount) { if (wantsToChangeZapAmount) {
UpdateZapAmountDialog( UpdateZapAmountDialog(
@ -1101,12 +1093,12 @@ fun ZapReaction(
wantsToPay = persistentListOf() wantsToPay = persistentListOf()
scope.launch { scope.launch {
zappingProgress = 0f zappingProgress = 0f
showErrorMessageDialog = showErrorMessageDialog + it errorViewModel.add(it)
} }
}, },
justShowError = { justShowError = {
scope.launch { scope.launch {
showErrorMessageDialog = showErrorMessageDialog + it errorViewModel.add(it)
} }
}, },
) )
@ -1115,10 +1107,10 @@ fun ZapReaction(
if (wantsToSetCustomZap) { if (wantsToSetCustomZap) {
ZapCustomDialog( ZapCustomDialog(
onClose = { wantsToSetCustomZap = false }, onClose = { wantsToSetCustomZap = false },
onError = { _, message -> onError = { _, message, user ->
scope.launch { scope.launch {
zappingProgress = 0f zappingProgress = 0f
showErrorMessageDialog = showErrorMessageDialog + message errorViewModel.add(message, user)
} }
}, },
onProgress = { scope.launch(Dispatchers.Main) { zappingProgress = it } }, onProgress = { scope.launch(Dispatchers.Main) { zappingProgress = it } },
@ -1168,7 +1160,7 @@ fun zapClick(
context: Context, context: Context,
onZappingProgress: (Float) -> Unit, onZappingProgress: (Float) -> Unit,
onMultipleChoices: () -> Unit, onMultipleChoices: () -> Unit,
onError: (String, String) -> Unit, onError: (String, String, User?) -> Unit,
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit, onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
) { ) {
if (baseNote.isDraft()) { if (baseNote.isDraft()) {
@ -1564,7 +1556,7 @@ fun ZapAmountChoicePopup(
popupYOffset: Dp, popupYOffset: Dp,
onDismiss: () -> Unit, onDismiss: () -> Unit,
onChangeAmount: () -> Unit, onChangeAmount: () -> Unit,
onError: (title: String, text: String) -> Unit, onError: (title: String, text: String, user: User?) -> Unit,
onProgress: (percent: Float) -> Unit, onProgress: (percent: Float) -> Unit,
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit, onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
) { ) {
@ -1583,7 +1575,7 @@ fun ZapAmountChoicePopup(
popupYOffset: Dp, popupYOffset: Dp,
onDismiss: () -> Unit, onDismiss: () -> Unit,
onChangeAmount: () -> Unit, onChangeAmount: () -> Unit,
onError: (title: String, text: String) -> Unit, onError: (title: String, text: String, user: User?) -> Unit,
onProgress: (percent: Float) -> Unit, onProgress: (percent: Float) -> Unit,
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit, onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
) { ) {
@ -1600,7 +1592,7 @@ fun ZapAmountChoicePopup(
popupYOffset: Dp, popupYOffset: Dp,
visibilityState: MutableTransitionState<Boolean>, visibilityState: MutableTransitionState<Boolean>,
onChangeAmount: () -> Unit, onChangeAmount: () -> Unit,
onError: (title: String, text: String) -> Unit, onError: (title: String, text: String, user: User?) -> Unit,
onProgress: (percent: Float) -> Unit, onProgress: (percent: Float) -> Unit,
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit, onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
) { ) {

View File

@ -25,27 +25,19 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Done
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -56,7 +48,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
@ -72,6 +63,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
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.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.service.ZapPaymentHandler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
@ -82,7 +74,6 @@ import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size16dp
import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.Size55dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.ZeroPadding import com.vitorpamplona.amethyst.ui.theme.ZeroPadding
@ -112,7 +103,7 @@ class ZapOptionstViewModel : ViewModel() {
@Composable @Composable
fun ZapCustomDialog( fun ZapCustomDialog(
onClose: () -> Unit, onClose: () -> Unit,
onError: (title: String, text: String) -> Unit, onError: (title: String, text: String, user: User?) -> Unit,
onProgress: (percent: Float) -> Unit, onProgress: (percent: Float) -> Unit,
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit, onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
@ -290,66 +281,21 @@ fun ZapButton(
} }
} }
@Composable
fun ErrorMessageDialog(
title: String,
textContent: String,
buttonColors: ButtonColors = ButtonDefaults.buttonColors(),
onClickStartMessage: (() -> Unit)? = null,
onDismiss: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(title) },
text = { SelectionContainer { Text(textContent) } },
confirmButton = {
Row(
modifier = Modifier.padding(vertical = 8.dp).fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
onClickStartMessage?.let {
TextButton(onClick = onClickStartMessage) {
Icon(
painter = painterResource(R.drawable.ic_dm),
contentDescription = null,
)
Spacer(StdHorzSpacer)
Text(stringRes(R.string.error_dialog_talk_to_user))
}
}
Button(
onClick = onDismiss,
colors = buttonColors,
contentPadding = PaddingValues(horizontal = Size16dp),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Outlined.Done,
contentDescription = null,
)
Spacer(StdHorzSpacer)
Text(stringRes(R.string.error_dialog_button_ok))
}
}
}
},
)
}
@Composable @Composable
fun PayViaIntentDialog( fun PayViaIntentDialog(
payingInvoices: ImmutableList<ZapPaymentHandler.Payable>, payingInvoices: ImmutableList<ZapPaymentHandler.Payable>,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
onClose: () -> Unit, onClose: () -> Unit,
onError: (String) -> Unit, onError: (UserBasedErrorMessage) -> Unit,
justShowError: (String) -> Unit, justShowError: (UserBasedErrorMessage) -> Unit,
) { ) {
val context = LocalContext.current val context = LocalContext.current
if (payingInvoices.size == 1) { if (payingInvoices.size == 1) {
payViaIntent(payingInvoices.first().invoice, context, onClose, onError) val payable = payingInvoices.first()
payViaIntent(payable.invoice, context, onClose) {
onError(UserBasedErrorMessage(it, payable.user))
}
} else { } else {
Dialog( Dialog(
onDismissRequest = onClose, onDismissRequest = onClose,
@ -370,15 +316,15 @@ fun PayViaIntentDialog(
Spacer(modifier = DoubleVertSpacer) Spacer(modifier = DoubleVertSpacer)
payingInvoices.forEachIndexed { index, it -> payingInvoices.forEachIndexed { index, payable ->
val paid = remember { mutableStateOf(false) } val paid = remember { mutableStateOf(false) }
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = Size10dp), modifier = Modifier.padding(vertical = Size10dp),
) { ) {
if (it.user != null) { if (payable.user != null) {
BaseUserPicture(it.user, Size55dp, accountViewModel = accountViewModel) BaseUserPicture(payable.user, Size55dp, accountViewModel = accountViewModel)
} else { } else {
DisplayBlankAuthor(size = Size55dp, accountViewModel = accountViewModel) DisplayBlankAuthor(size = Size55dp, accountViewModel = accountViewModel)
} }
@ -386,8 +332,8 @@ fun PayViaIntentDialog(
Spacer(modifier = DoubleHorzSpacer) Spacer(modifier = DoubleHorzSpacer)
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
if (it.user != null) { if (payable.user != null) {
UsernameDisplay(it.user, accountViewModel = accountViewModel) UsernameDisplay(payable.user, accountViewModel = accountViewModel)
} else { } else {
Text( Text(
text = stringRes(id = R.string.wallet_number, index + 1), text = stringRes(id = R.string.wallet_number, index + 1),
@ -399,7 +345,7 @@ fun PayViaIntentDialog(
} }
Row { Row {
Text( Text(
text = showAmount((it.amountMilliSats / 1000.0f).toBigDecimal()), text = showAmount((payable.amountMilliSats / 1000.0f).toBigDecimal()),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
@ -419,7 +365,9 @@ fun PayViaIntentDialog(
Spacer(modifier = DoubleHorzSpacer) Spacer(modifier = DoubleHorzSpacer)
PayButton(isActive = !paid.value) { PayButton(isActive = !paid.value) {
payViaIntent(it.invoice, context, { paid.value = true }, justShowError) payViaIntent(payable.invoice, context, { paid.value = true }) {
justShowError(UserBasedErrorMessage(it, payable.user))
}
} }
} }
} }

View File

@ -58,10 +58,12 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
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.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.service.ZapPaymentHandler
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
import com.vitorpamplona.amethyst.ui.components.ClickableText import com.vitorpamplona.amethyst.ui.components.ClickableText
@ -69,11 +71,11 @@ import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.routeFor import com.vitorpamplona.amethyst.ui.navigation.routeFor
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
import com.vitorpamplona.amethyst.ui.note.CloseIcon import com.vitorpamplona.amethyst.ui.note.CloseIcon
import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog import com.vitorpamplona.amethyst.ui.note.MultiUserErrorMessageDialog
import com.vitorpamplona.amethyst.ui.note.ObserveZapIcon import com.vitorpamplona.amethyst.ui.note.ObserveZapIcon
import com.vitorpamplona.amethyst.ui.note.PayViaIntentDialog import com.vitorpamplona.amethyst.ui.note.PayViaIntentDialog
import com.vitorpamplona.amethyst.ui.note.UserBasedErrorMessageViewModel
import com.vitorpamplona.amethyst.ui.note.ZapAmountChoicePopup import com.vitorpamplona.amethyst.ui.note.ZapAmountChoicePopup
import com.vitorpamplona.amethyst.ui.note.ZapIcon import com.vitorpamplona.amethyst.ui.note.ZapIcon
import com.vitorpamplona.amethyst.ui.note.ZappedIcon import com.vitorpamplona.amethyst.ui.note.ZappedIcon
@ -291,7 +293,7 @@ fun ZapDonationButton(
nav: INav, nav: INav,
) { ) {
var wantsToZap by remember { mutableStateOf<ImmutableList<Long>?>(null) } var wantsToZap by remember { mutableStateOf<ImmutableList<Long>?>(null) }
var showErrorMessageDialog by remember { mutableStateOf<String?>(null) } val errorViewModel: UserBasedErrorMessageViewModel = viewModel()
var wantsToPay by var wantsToPay by
remember(baseNote) { remember(baseNote) {
mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>( mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>(
@ -315,10 +317,10 @@ fun ZapDonationButton(
scope.launch { zappingProgress = progress } scope.launch { zappingProgress = progress }
}, },
onMultipleChoices = { options -> wantsToZap = options.toImmutableList() }, onMultipleChoices = { options -> wantsToZap = options.toImmutableList() },
onError = { _, message -> onError = { _, message, toUser ->
scope.launch { scope.launch {
zappingProgress = 0f zappingProgress = 0f
showErrorMessageDialog = message errorViewModel.add(message, toUser)
} }
}, },
onPayViaIntent = { wantsToPay = it }, onPayViaIntent = { wantsToPay = it },
@ -339,10 +341,10 @@ fun ZapDonationButton(
onChangeAmount = { onChangeAmount = {
wantsToZap = null wantsToZap = null
}, },
onError = { _, message -> onError = { _, message, user ->
scope.launch { scope.launch {
zappingProgress = 0f zappingProgress = 0f
showErrorMessageDialog = message errorViewModel.add(message, user)
} }
}, },
onProgress = { onProgress = {
@ -352,21 +354,12 @@ fun ZapDonationButton(
) )
} }
if (showErrorMessageDialog != null) { MultiUserErrorMessageDialog(
ErrorMessageDialog( title = stringRes(id = R.string.error_dialog_zap_error),
title = stringRes(id = R.string.error_dialog_zap_error), model = errorViewModel,
textContent = showErrorMessageDialog ?: "", accountViewModel = accountViewModel,
onClickStartMessage = { nav = nav,
baseNote.author?.let { )
scope.launch(Dispatchers.IO) {
val route = routeToMessage(it, showErrorMessageDialog, accountViewModel)
nav.nav(route)
}
}
},
onDismiss = { showErrorMessageDialog = null },
)
}
if (wantsToPay.isNotEmpty()) { if (wantsToPay.isNotEmpty()) {
PayViaIntentDialog( PayViaIntentDialog(
@ -377,12 +370,12 @@ fun ZapDonationButton(
wantsToPay = persistentListOf() wantsToPay = persistentListOf()
scope.launch { scope.launch {
zappingProgress = 0f zappingProgress = 0f
showErrorMessageDialog = it errorViewModel.add(it)
} }
}, },
justShowError = { justShowError = {
scope.launch { scope.launch {
showErrorMessageDialog = it errorViewModel.add(it)
} }
}, },
) )
@ -444,7 +437,7 @@ fun customZapClick(
context: Context, context: Context,
onZappingProgress: (Float) -> Unit, onZappingProgress: (Float) -> Unit,
onMultipleChoices: (List<Long>) -> Unit, onMultipleChoices: (List<Long>) -> Unit,
onError: (String, String) -> Unit, onError: (String, String, User?) -> Unit,
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit, onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
) { ) {
if (baseNote.isDraft()) { if (baseNote.isDraft()) {

View File

@ -506,6 +506,12 @@ class AccountViewModel(
} }
} }
class DecryptedInfo(
val zapRequest: Note,
val zapEvent: Note?,
val info: ZapAmountCommentNotification,
)
fun decryptAmountMessageInGroup( fun decryptAmountMessageInGroup(
zaps: ImmutableList<CombinedZap>, zaps: ImmutableList<CombinedZap>,
onNewState: (ImmutableList<ZapAmountCommentNotification>) -> Unit, onNewState: (ImmutableList<ZapAmountCommentNotification>) -> Unit,
@ -524,17 +530,19 @@ class AccountViewModel(
) )
}.toMutableMap() }.toMutableMap()
collectSuccessfulOperations<CombinedZap, ZapAmountCommentNotification>( collectSuccessfulOperations<CombinedZap, DecryptedInfo>(
items = zaps.filter { (it.request.event as? LnZapRequestEvent)?.isPrivateZap() == true }, items = zaps.filter { (it.request.event as? LnZapRequestEvent)?.isPrivateZap() == true },
runRequestFor = { next, onReady -> runRequestFor = { next, onReady ->
checkNotInMainThread() checkNotInMainThread()
innerDecryptAmountMessage(next.request, next.response, onReady) innerDecryptAmountMessage(next.request, next.response) {
onReady(DecryptedInfo(next.request, next.response, it))
}
}, },
) { ) {
checkNotInMainThread() checkNotInMainThread()
it.forEach { decrypted -> initialResults[decrypted.key.request] = decrypted.value } it.forEach { decrypted -> initialResults[decrypted.zapRequest] = decrypted.info }
onNewState(initialResults.values.toImmutableList()) onNewState(initialResults.values.toImmutableList())
} }
@ -628,13 +636,15 @@ class AccountViewModel(
) )
}.toMutableMap() }.toMutableMap()
collectSuccessfulOperations<Pair<Note, Note?>, ZapAmountCommentNotification>( collectSuccessfulOperations<Pair<Note, Note?>, DecryptedInfo>(
items = myList, items = myList,
runRequestFor = { next, onReady -> runRequestFor = { next, onReady ->
innerDecryptAmountMessage(next.first, next.second, onReady) innerDecryptAmountMessage(next.first, next.second) {
onReady(DecryptedInfo(next.first, next.second, it))
}
}, },
) { ) {
it.forEach { decrypted -> initialResults[decrypted.key.first] = decrypted.value } it.forEach { decrypted -> initialResults[decrypted.zapRequest] = decrypted.info }
onNewState(initialResults.values.toImmutableList()) onNewState(initialResults.values.toImmutableList())
} }
@ -693,7 +703,7 @@ class AccountViewModel(
message: String, message: String,
context: Context, context: Context,
showErrorIfNoLnAddress: Boolean = true, showErrorIfNoLnAddress: Boolean = true,
onError: (String, String) -> Unit, onError: (String, String, User?) -> Unit,
onProgress: (percent: Float) -> Unit, onProgress: (percent: Float) -> Unit,
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit, onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
zapType: LnZapEvent.ZapType? = null, zapType: LnZapEvent.ZapType? = null,

View File

@ -69,12 +69,12 @@ import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
import com.vitorpamplona.amethyst.ui.note.DVMCard import com.vitorpamplona.amethyst.ui.note.DVMCard
import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog import com.vitorpamplona.amethyst.ui.note.MultiUserErrorMessageDialog
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
import com.vitorpamplona.amethyst.ui.note.ObserveZapIcon import com.vitorpamplona.amethyst.ui.note.ObserveZapIcon
import com.vitorpamplona.amethyst.ui.note.PayViaIntentDialog import com.vitorpamplona.amethyst.ui.note.PayViaIntentDialog
import com.vitorpamplona.amethyst.ui.note.UserBasedErrorMessageViewModel
import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent
import com.vitorpamplona.amethyst.ui.note.ZapAmountChoicePopup import com.vitorpamplona.amethyst.ui.note.ZapAmountChoicePopup
import com.vitorpamplona.amethyst.ui.note.ZapIcon import com.vitorpamplona.amethyst.ui.note.ZapIcon
@ -439,7 +439,7 @@ fun ZapDVMButton(
val noteAuthor = baseNote.author ?: return val noteAuthor = baseNote.author ?: return
var wantsToZap by remember { mutableStateOf<List<Long>?>(null) } var wantsToZap by remember { mutableStateOf<List<Long>?>(null) }
var showErrorMessageDialog by remember { mutableStateOf<String?>(null) } val errorViewModel: UserBasedErrorMessageViewModel = viewModel()
var wantsToPay by var wantsToPay by
remember(baseNote) { remember(baseNote) {
mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>( mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>(
@ -466,10 +466,10 @@ fun ZapDVMButton(
scope.launch { zappingProgress = progress } scope.launch { zappingProgress = progress }
}, },
onMultipleChoices = { options -> wantsToZap = options }, onMultipleChoices = { options -> wantsToZap = options },
onError = { _, message -> onError = { _, message, toUser ->
scope.launch { scope.launch {
zappingProgress = 0f zappingProgress = 0f
showErrorMessageDialog = message errorViewModel.add(message, toUser)
} }
}, },
onPayViaIntent = { wantsToPay = it }, onPayViaIntent = { wantsToPay = it },
@ -490,10 +490,10 @@ fun ZapDVMButton(
onChangeAmount = { onChangeAmount = {
wantsToZap = null wantsToZap = null
}, },
onError = { _, message -> onError = { _, message, user ->
scope.launch { scope.launch {
zappingProgress = 0f zappingProgress = 0f
showErrorMessageDialog = message errorViewModel.add(message, user)
} }
}, },
onProgress = { onProgress = {
@ -503,21 +503,12 @@ fun ZapDVMButton(
) )
} }
if (showErrorMessageDialog != null) { MultiUserErrorMessageDialog(
ErrorMessageDialog( title = stringRes(id = R.string.error_dialog_zap_error),
title = stringRes(id = R.string.error_dialog_zap_error), model = errorViewModel,
textContent = showErrorMessageDialog ?: "", accountViewModel,
onClickStartMessage = { nav,
baseNote.author?.let { )
scope.launch(Dispatchers.IO) {
val route = routeToMessage(it, showErrorMessageDialog, accountViewModel)
nav.nav(route)
}
}
},
onDismiss = { showErrorMessageDialog = null },
)
}
if (wantsToPay.isNotEmpty()) { if (wantsToPay.isNotEmpty()) {
PayViaIntentDialog( PayViaIntentDialog(
@ -528,12 +519,12 @@ fun ZapDVMButton(
wantsToPay = persistentListOf() wantsToPay = persistentListOf()
scope.launch { scope.launch {
zappingProgress = 0f zappingProgress = 0f
showErrorMessageDialog = it errorViewModel.add(it)
} }
}, },
justShowError = { justShowError = {
scope.launch { scope.launch {
showErrorMessageDialog = it errorViewModel.add(it)
} }
}, },
) )

View File

@ -726,6 +726,7 @@
<string name="error_dialog_zap_error">Unable to send zap</string> <string name="error_dialog_zap_error">Unable to send zap</string>
<string name="error_dialog_talk_to_user">Message the User</string> <string name="error_dialog_talk_to_user">Message the User</string>
<string name="error_dialog_talk_to_user_name">Message %1$s</string>
<string name="error_dialog_button_ok">OK</string> <string name="error_dialog_button_ok">OK</string>
<string name="relay_information_document_error_assemble_url">Failed to reach %1$s: %2$s</string> <string name="relay_information_document_error_assemble_url">Failed to reach %1$s: %2$s</string>
@ -801,6 +802,7 @@
<string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct">Could not resolve %1$s. Check if you are connected, if the server is up and if the lightning address %2$s is correct</string> <string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct">Could not resolve %1$s. Check if you are connected, if the server is up and if the lightning address %2$s is correct</string>
<string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct_exception">Could not resolve %1$s. Check if you are connected, if the server is up and if the lightning address %2$s is correct.\n\nException was: %3$s</string> <string name="could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct_exception">Could not resolve %1$s. Check if you are connected, if the server is up and if the lightning address %2$s is correct.\n\nException was: %3$s</string>
<string name="could_not_fetch_invoice_from">Could not fetch invoice from %1$s</string> <string name="could_not_fetch_invoice_from">Could not fetch invoice from %1$s</string>
<string name="could_not_fetch_invoice_from_details">Could not fetch invoice from %1$s: %2$s</string>
<string name="error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup">Error Parsing JSON from Lightning Address. Check the user\'s lightning setup</string> <string name="error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup">Error Parsing JSON from Lightning Address. Check the user\'s lightning setup</string>
<string name="error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup_with_user">Error Parsing JSON from %1$s. Check the user\'s lightning setup</string> <string name="error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup_with_user">Error Parsing JSON from %1$s. Check the user\'s lightning setup</string>