mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-03-17 21:31:57 +01:00
Refactoring Zap Error message screen to allow sending messages directly to each receiver with an error
This commit is contained in:
parent
497ae937fd
commit
160d4722c0
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
) {
|
||||
|
@ -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,
|
||||
) {
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()) {
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
@ -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>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user