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

View File

@ -24,12 +24,14 @@ import android.content.Context
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.HttpStatusMessages
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.okhttp.HttpClientManager
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
import com.vitorpamplona.quartz.encoders.Lud06
import okhttp3.Request
import okhttp3.Response
import java.math.BigDecimal
import java.math.RoundingMode
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,
url,
lnaddress,
it.code.toString(),
errorMessage(it, context),
),
)
}
@ -154,12 +156,36 @@ class LightningAddressResolver {
} else {
onError(
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(
lnaddress: String,
milliSats: Long,
@ -195,7 +221,7 @@ class LightningAddressResolver {
null
}
val callback = lnurlp?.get("callback")?.asText()
val callback = lnurlp?.get("callback")?.asText()?.ifBlank { null }
if (callback == null) {
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.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
@ -559,7 +560,7 @@ fun ZapVote(
poolOption.option,
"",
context,
onError = { title, message ->
onError = { title, message, user ->
zappingProgress = 0f
showErrorMessageDialog = StringToastMsg(title, message)
},
@ -583,7 +584,7 @@ fun ZapVote(
zappingProgress = 0f
},
onChangeAmount = { wantsToZap = false },
onError = { title, message ->
onError = { title, message, user ->
showErrorMessageDialog = StringToastMsg(title, message)
zappingProgress = 0f
},
@ -604,7 +605,7 @@ fun ZapVote(
showErrorMessageDialog =
StringToastMsg(
stringRes(context, R.string.error_dialog_zap_error),
it,
it.error,
)
}
},
@ -613,7 +614,7 @@ fun ZapVote(
showErrorMessageDialog =
StringToastMsg(
stringRes(context, R.string.error_dialog_zap_error),
it,
it.error,
)
}
},
@ -681,7 +682,7 @@ fun FilteredZapAmountChoicePopup(
pollOption: Int,
onDismiss: () -> Unit,
onChangeAmount: () -> Unit,
onError: (title: String, text: String) -> Unit,
onError: (title: String, text: String, toUser: User?) -> Unit,
onProgress: (percent: Float) -> Unit,
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
) {

View File

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

View File

@ -25,27 +25,19 @@ import android.content.Intent
import android.net.Uri
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.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.SelectionContainer
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.ButtonColors
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@ -56,7 +48,6 @@ 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
@ -72,6 +63,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
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.DoubleVertSpacer
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.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.ZeroPadding
@ -112,7 +103,7 @@ class ZapOptionstViewModel : ViewModel() {
@Composable
fun ZapCustomDialog(
onClose: () -> Unit,
onError: (title: String, text: String) -> Unit,
onError: (title: String, text: String, user: User?) -> Unit,
onProgress: (percent: Float) -> Unit,
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
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
fun PayViaIntentDialog(
payingInvoices: ImmutableList<ZapPaymentHandler.Payable>,
accountViewModel: AccountViewModel,
onClose: () -> Unit,
onError: (String) -> Unit,
justShowError: (String) -> Unit,
onError: (UserBasedErrorMessage) -> Unit,
justShowError: (UserBasedErrorMessage) -> Unit,
) {
val context = LocalContext.current
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 {
Dialog(
onDismissRequest = onClose,
@ -370,15 +316,15 @@ fun PayViaIntentDialog(
Spacer(modifier = DoubleVertSpacer)
payingInvoices.forEachIndexed { index, it ->
payingInvoices.forEachIndexed { index, payable ->
val paid = remember { mutableStateOf(false) }
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = Size10dp),
) {
if (it.user != null) {
BaseUserPicture(it.user, Size55dp, accountViewModel = accountViewModel)
if (payable.user != null) {
BaseUserPicture(payable.user, Size55dp, accountViewModel = accountViewModel)
} else {
DisplayBlankAuthor(size = Size55dp, accountViewModel = accountViewModel)
}
@ -386,8 +332,8 @@ fun PayViaIntentDialog(
Spacer(modifier = DoubleHorzSpacer)
Column(modifier = Modifier.weight(1f)) {
if (it.user != null) {
UsernameDisplay(it.user, accountViewModel = accountViewModel)
if (payable.user != null) {
UsernameDisplay(payable.user, accountViewModel = accountViewModel)
} else {
Text(
text = stringRes(id = R.string.wallet_number, index + 1),
@ -399,7 +345,7 @@ fun PayViaIntentDialog(
}
Row {
Text(
text = showAmount((it.amountMilliSats / 1000.0f).toBigDecimal()),
text = showAmount((payable.amountMilliSats / 1000.0f).toBigDecimal()),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
fontWeight = FontWeight.Bold,
@ -419,7 +365,9 @@ fun PayViaIntentDialog(
Spacer(modifier = DoubleHorzSpacer)
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.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
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.INav
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.ErrorMessageDialog
import com.vitorpamplona.amethyst.ui.note.MultiUserErrorMessageDialog
import com.vitorpamplona.amethyst.ui.note.ObserveZapIcon
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.ZapIcon
import com.vitorpamplona.amethyst.ui.note.ZappedIcon
@ -291,7 +293,7 @@ fun ZapDonationButton(
nav: INav,
) {
var wantsToZap by remember { mutableStateOf<ImmutableList<Long>?>(null) }
var showErrorMessageDialog by remember { mutableStateOf<String?>(null) }
val errorViewModel: UserBasedErrorMessageViewModel = viewModel()
var wantsToPay by
remember(baseNote) {
mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>(
@ -315,10 +317,10 @@ fun ZapDonationButton(
scope.launch { zappingProgress = progress }
},
onMultipleChoices = { options -> wantsToZap = options.toImmutableList() },
onError = { _, message ->
onError = { _, message, toUser ->
scope.launch {
zappingProgress = 0f
showErrorMessageDialog = message
errorViewModel.add(message, toUser)
}
},
onPayViaIntent = { wantsToPay = it },
@ -339,10 +341,10 @@ fun ZapDonationButton(
onChangeAmount = {
wantsToZap = null
},
onError = { _, message ->
onError = { _, message, user ->
scope.launch {
zappingProgress = 0f
showErrorMessageDialog = message
errorViewModel.add(message, user)
}
},
onProgress = {
@ -352,21 +354,12 @@ fun ZapDonationButton(
)
}
if (showErrorMessageDialog != null) {
ErrorMessageDialog(
title = stringRes(id = R.string.error_dialog_zap_error),
textContent = showErrorMessageDialog ?: "",
onClickStartMessage = {
baseNote.author?.let {
scope.launch(Dispatchers.IO) {
val route = routeToMessage(it, showErrorMessageDialog, accountViewModel)
nav.nav(route)
}
}
},
onDismiss = { showErrorMessageDialog = null },
)
}
MultiUserErrorMessageDialog(
title = stringRes(id = R.string.error_dialog_zap_error),
model = errorViewModel,
accountViewModel = accountViewModel,
nav = nav,
)
if (wantsToPay.isNotEmpty()) {
PayViaIntentDialog(
@ -377,12 +370,12 @@ fun ZapDonationButton(
wantsToPay = persistentListOf()
scope.launch {
zappingProgress = 0f
showErrorMessageDialog = it
errorViewModel.add(it)
}
},
justShowError = {
scope.launch {
showErrorMessageDialog = it
errorViewModel.add(it)
}
},
)
@ -444,7 +437,7 @@ fun customZapClick(
context: Context,
onZappingProgress: (Float) -> Unit,
onMultipleChoices: (List<Long>) -> Unit,
onError: (String, String) -> Unit,
onError: (String, String, User?) -> Unit,
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
) {
if (baseNote.isDraft()) {

View File

@ -506,6 +506,12 @@ class AccountViewModel(
}
}
class DecryptedInfo(
val zapRequest: Note,
val zapEvent: Note?,
val info: ZapAmountCommentNotification,
)
fun decryptAmountMessageInGroup(
zaps: ImmutableList<CombinedZap>,
onNewState: (ImmutableList<ZapAmountCommentNotification>) -> Unit,
@ -524,17 +530,19 @@ class AccountViewModel(
)
}.toMutableMap()
collectSuccessfulOperations<CombinedZap, ZapAmountCommentNotification>(
collectSuccessfulOperations<CombinedZap, DecryptedInfo>(
items = zaps.filter { (it.request.event as? LnZapRequestEvent)?.isPrivateZap() == true },
runRequestFor = { next, onReady ->
checkNotInMainThread()
innerDecryptAmountMessage(next.request, next.response, onReady)
innerDecryptAmountMessage(next.request, next.response) {
onReady(DecryptedInfo(next.request, next.response, it))
}
},
) {
checkNotInMainThread()
it.forEach { decrypted -> initialResults[decrypted.key.request] = decrypted.value }
it.forEach { decrypted -> initialResults[decrypted.zapRequest] = decrypted.info }
onNewState(initialResults.values.toImmutableList())
}
@ -628,13 +636,15 @@ class AccountViewModel(
)
}.toMutableMap()
collectSuccessfulOperations<Pair<Note, Note?>, ZapAmountCommentNotification>(
collectSuccessfulOperations<Pair<Note, Note?>, DecryptedInfo>(
items = myList,
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())
}
@ -693,7 +703,7 @@ class AccountViewModel(
message: String,
context: Context,
showErrorIfNoLnAddress: Boolean = true,
onError: (String, String) -> Unit,
onError: (String, String, User?) -> Unit,
onProgress: (percent: Float) -> Unit,
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
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.RefresheableBox
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.ErrorMessageDialog
import com.vitorpamplona.amethyst.ui.note.MultiUserErrorMessageDialog
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
import com.vitorpamplona.amethyst.ui.note.ObserveZapIcon
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.ZapAmountChoicePopup
import com.vitorpamplona.amethyst.ui.note.ZapIcon
@ -439,7 +439,7 @@ fun ZapDVMButton(
val noteAuthor = baseNote.author ?: return
var wantsToZap by remember { mutableStateOf<List<Long>?>(null) }
var showErrorMessageDialog by remember { mutableStateOf<String?>(null) }
val errorViewModel: UserBasedErrorMessageViewModel = viewModel()
var wantsToPay by
remember(baseNote) {
mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>(
@ -466,10 +466,10 @@ fun ZapDVMButton(
scope.launch { zappingProgress = progress }
},
onMultipleChoices = { options -> wantsToZap = options },
onError = { _, message ->
onError = { _, message, toUser ->
scope.launch {
zappingProgress = 0f
showErrorMessageDialog = message
errorViewModel.add(message, toUser)
}
},
onPayViaIntent = { wantsToPay = it },
@ -490,10 +490,10 @@ fun ZapDVMButton(
onChangeAmount = {
wantsToZap = null
},
onError = { _, message ->
onError = { _, message, user ->
scope.launch {
zappingProgress = 0f
showErrorMessageDialog = message
errorViewModel.add(message, user)
}
},
onProgress = {
@ -503,21 +503,12 @@ fun ZapDVMButton(
)
}
if (showErrorMessageDialog != null) {
ErrorMessageDialog(
title = stringRes(id = R.string.error_dialog_zap_error),
textContent = showErrorMessageDialog ?: "",
onClickStartMessage = {
baseNote.author?.let {
scope.launch(Dispatchers.IO) {
val route = routeToMessage(it, showErrorMessageDialog, accountViewModel)
nav.nav(route)
}
}
},
onDismiss = { showErrorMessageDialog = null },
)
}
MultiUserErrorMessageDialog(
title = stringRes(id = R.string.error_dialog_zap_error),
model = errorViewModel,
accountViewModel,
nav,
)
if (wantsToPay.isNotEmpty()) {
PayViaIntentDialog(
@ -528,12 +519,12 @@ fun ZapDVMButton(
wantsToPay = persistentListOf()
scope.launch {
zappingProgress = 0f
showErrorMessageDialog = it
errorViewModel.add(it)
}
},
justShowError = {
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_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="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_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_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_with_user">Error Parsing JSON from %1$s. Check the user\'s lightning setup</string>