diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ParallelUtils.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ParallelUtils.kt index a157eb033..4b1f7da46 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ParallelUtils.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ParallelUtils.kt @@ -72,8 +72,8 @@ suspend inline fun tryAndWait( suspend fun collectSuccessfulOperations( items: List, runRequestFor: (T, (K) -> Unit) -> Unit, - output: MutableMap = mutableMapOf(), - onReady: suspend (MutableMap) -> Unit, + output: MutableList = mutableListOf(), + onReady: suspend (List) -> Unit, ) { if (items.isEmpty()) { onReady(output) @@ -87,7 +87,7 @@ suspend fun collectSuccessfulOperations( } if (result != null) { - output[it] = result + output.add(result) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt index 64efe49f4..b1b639c18 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt @@ -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) -> 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, - onAllDone: suspend (MutableMap) -> Unit, + onAllDone: suspend (List) -> Unit, ) { val authorRelayList = note.author @@ -194,13 +190,13 @@ class ZapPaymentHandler( )?.readRelays() }?.toSet() - collectSuccessfulOperations( + collectSuccessfulOperations( 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>, + requests: List, 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, AssembleInvoiceReturn>) -> Unit, + onAllDone: suspend (List) -> Unit, ) { var progressAllPayments = 0.00f - val totalWeight = invoices.sumOf { it.first.weight } + val totalWeight = requests.sumOf { it.inputSetup.weight } - collectSuccessfulOperations, AssembleInvoiceReturn>( - items = invoices, - runRequestFor = { splitZapRequestPair: Pair, onReady -> + collectSuccessfulOperations( + 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, + payables: List, note: Note, - onError: (String, String) -> Unit, + onError: (String, String, User?) -> Unit, onProgress: (percent: Float) -> Unit, context: Context, - onAllDone: suspend (MutableMap) -> Unit, + onAllDone: suspend (List) -> Unit, ) { var progressAllPayments = 0.00f - collectSuccessfulOperations( - items = invoices, - runRequestFor = { invoice: String, onReady -> + collectSuccessfulOperations( + 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, ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt index c6ae303eb..089a16e4f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt @@ -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( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ErrorMessageDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ErrorMessageDialog.kt new file mode 100644 index 000000000..a9a691212 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ErrorMessageDialog.kt @@ -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)) + } + } + } + }, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiUserMessageDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiUserMessageDialog.kt new file mode 100644 index 000000000..5a1eb9d81 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiUserMessageDialog.kt @@ -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>(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) + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt index 653872c22..4058efc27 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt @@ -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) -> Unit, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 1ee707aa6..f4e1193de 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -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>(emptyList()) } + val errorViewModel: UserBasedErrorMessageViewModel = viewModel() var wantsToPay by remember(baseNote) { mutableStateOf>( @@ -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) -> 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) -> 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) -> Unit, ) { @@ -1600,7 +1592,7 @@ fun ZapAmountChoicePopup( popupYOffset: Dp, visibilityState: MutableTransitionState, onChangeAmount: () -> Unit, - onError: (title: String, text: String) -> Unit, + onError: (title: String, text: String, user: User?) -> Unit, onProgress: (percent: Float) -> Unit, onPayViaIntent: (ImmutableList) -> Unit, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt index eb1ddab57..ccad6e16b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt @@ -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) -> 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, 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)) + } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ZapTheDevsCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ZapTheDevsCard.kt index c16749b89..e48cf9721 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ZapTheDevsCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ZapTheDevsCard.kt @@ -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?>(null) } - var showErrorMessageDialog by remember { mutableStateOf(null) } + val errorViewModel: UserBasedErrorMessageViewModel = viewModel() var wantsToPay by remember(baseNote) { mutableStateOf>( @@ -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) -> Unit, - onError: (String, String) -> Unit, + onError: (String, String, User?) -> Unit, onPayViaIntent: (ImmutableList) -> Unit, ) { if (baseNote.isDraft()) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index b7e36f0b1..b1e9b2ce6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -506,6 +506,12 @@ class AccountViewModel( } } + class DecryptedInfo( + val zapRequest: Note, + val zapEvent: Note?, + val info: ZapAmountCommentNotification, + ) + fun decryptAmountMessageInGroup( zaps: ImmutableList, onNewState: (ImmutableList) -> Unit, @@ -524,17 +530,19 @@ class AccountViewModel( ) }.toMutableMap() - collectSuccessfulOperations( + collectSuccessfulOperations( 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, ZapAmountCommentNotification>( + collectSuccessfulOperations, 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) -> Unit, zapType: LnZapEvent.ZapType? = null, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt index 9ccdb8d2c..8ca18ab14 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt @@ -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?>(null) } - var showErrorMessageDialog by remember { mutableStateOf(null) } + val errorViewModel: UserBasedErrorMessageViewModel = viewModel() var wantsToPay by remember(baseNote) { mutableStateOf>( @@ -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) } }, ) diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 2b20ea93e..731336745 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -726,6 +726,7 @@ Unable to send zap Message the User + Message %1$s OK Failed to reach %1$s: %2$s @@ -801,6 +802,7 @@ Could not resolve %1$s. Check if you are connected, if the server is up and if the lightning address %2$s 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.\n\nException was: %3$s Could not fetch invoice from %1$s + Could not fetch invoice from %1$s: %2$s Error Parsing JSON from Lightning Address. Check the user\'s lightning setup Error Parsing JSON from %1$s. Check the user\'s lightning setup