- Fixes lack of zap updates when there is a private zap the user cannot decrypt or when a nwc payment fails.
- Moves the parallel processing to a utils class
This commit is contained in:
@@ -0,0 +1,92 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CancellableContinuation
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.joinAll
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launches an async coroutine for each item, runs the
|
||||||
|
* function and waits for everybody to finsih
|
||||||
|
*/
|
||||||
|
suspend fun <T> launchAndWaitAll(
|
||||||
|
items: List<T>,
|
||||||
|
asyncFunc: suspend (T) -> Unit,
|
||||||
|
) {
|
||||||
|
coroutineScope {
|
||||||
|
val jobs =
|
||||||
|
items.map { next ->
|
||||||
|
async {
|
||||||
|
asyncFunc(next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// runs in parallel to avoid overcrowding Amber.
|
||||||
|
withTimeoutOrNull(15000) {
|
||||||
|
jobs.joinAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the function and waits for 10 seconds for any result.
|
||||||
|
*/
|
||||||
|
suspend inline fun <T> tryAndWait(crossinline asyncFunc: (CancellableContinuation<T>) -> Unit): T? =
|
||||||
|
withTimeoutOrNull(10000) {
|
||||||
|
suspendCancellableCoroutine { continuation ->
|
||||||
|
asyncFunc(continuation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs an async coroutine for each one of the items,
|
||||||
|
* runs the request for that item,
|
||||||
|
* and gathers all the results in the output map.
|
||||||
|
*/
|
||||||
|
suspend fun <T, K> collectSuccessfulOperations(
|
||||||
|
items: List<T>,
|
||||||
|
runRequestFor: (T, (K) -> Unit) -> Unit,
|
||||||
|
output: MutableMap<T, K> = mutableMapOf(),
|
||||||
|
onReady: suspend (MutableMap<T, K>) -> Unit,
|
||||||
|
) {
|
||||||
|
if (items.isEmpty()) {
|
||||||
|
onReady(output)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
launchAndWaitAll(items) {
|
||||||
|
val result =
|
||||||
|
tryAndWait { continuation ->
|
||||||
|
runRequestFor(it) { result: K -> continuation.resume(result) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result != null) {
|
||||||
|
output[it] = result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onReady(output)
|
||||||
|
}
|
||||||
@@ -35,6 +35,7 @@ import com.vitorpamplona.amethyst.BuildConfig
|
|||||||
import com.vitorpamplona.amethyst.service.FileHeader
|
import com.vitorpamplona.amethyst.service.FileHeader
|
||||||
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource
|
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource
|
||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
|
import com.vitorpamplona.amethyst.tryAndWait
|
||||||
import com.vitorpamplona.amethyst.ui.tor.TorType
|
import com.vitorpamplona.amethyst.ui.tor.TorType
|
||||||
import com.vitorpamplona.ammolite.relays.Client
|
import com.vitorpamplona.ammolite.relays.Client
|
||||||
import com.vitorpamplona.ammolite.relays.Constants
|
import com.vitorpamplona.ammolite.relays.Constants
|
||||||
@@ -129,8 +130,6 @@ import kotlinx.coroutines.flow.transformLatest
|
|||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@@ -897,11 +896,9 @@ class Account(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun waitToDecrypt(peopleListFollows: GeneralListEvent): LiveFollowList? =
|
suspend fun waitToDecrypt(peopleListFollows: GeneralListEvent): LiveFollowList? =
|
||||||
withTimeoutOrNull(1000) {
|
tryAndWait { continuation ->
|
||||||
suspendCancellableCoroutine { continuation ->
|
decryptLiveFollows(peopleListFollows) {
|
||||||
decryptLiveFollows(peopleListFollows) {
|
continuation.resume(it)
|
||||||
continuation.resume(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -921,11 +918,9 @@ class Account(
|
|||||||
suspend fun decryptPeopleList(event: PeopleListEvent?): PeopleListEvent.UsersAndWords {
|
suspend fun decryptPeopleList(event: PeopleListEvent?): PeopleListEvent.UsersAndWords {
|
||||||
if (event == null) return PeopleListEvent.UsersAndWords()
|
if (event == null) return PeopleListEvent.UsersAndWords()
|
||||||
|
|
||||||
return withTimeoutOrNull(1000) {
|
return tryAndWait { continuation ->
|
||||||
suspendCancellableCoroutine { continuation ->
|
event.publicAndPrivateUsersAndWords(signer) {
|
||||||
event.publicAndPrivateUsersAndWords(signer) {
|
continuation.resume(it)
|
||||||
continuation.resume(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} ?: PeopleListEvent.UsersAndWords()
|
} ?: PeopleListEvent.UsersAndWords()
|
||||||
}
|
}
|
||||||
@@ -933,11 +928,9 @@ class Account(
|
|||||||
suspend fun decryptMuteList(event: MuteListEvent?): PeopleListEvent.UsersAndWords {
|
suspend fun decryptMuteList(event: MuteListEvent?): PeopleListEvent.UsersAndWords {
|
||||||
if (event == null) return PeopleListEvent.UsersAndWords()
|
if (event == null) return PeopleListEvent.UsersAndWords()
|
||||||
|
|
||||||
return withTimeoutOrNull(1000) {
|
return tryAndWait { continuation ->
|
||||||
suspendCancellableCoroutine { continuation ->
|
event.publicAndPrivateUsersAndWords(signer) {
|
||||||
event.publicAndPrivateUsersAndWords(signer) {
|
continuation.resume(it)
|
||||||
continuation.resume(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} ?: PeopleListEvent.UsersAndWords()
|
} ?: PeopleListEvent.UsersAndWords()
|
||||||
}
|
}
|
||||||
@@ -992,11 +985,9 @@ class Account(
|
|||||||
emit(null)
|
emit(null)
|
||||||
} else {
|
} else {
|
||||||
emit(
|
emit(
|
||||||
withTimeoutOrNull(1000) {
|
tryAndWait { continuation ->
|
||||||
suspendCancellableCoroutine { continuation ->
|
userState.user.latestBookmarkList?.privateTags(signer) {
|
||||||
userState.user.latestBookmarkList?.privateTags(signer) {
|
continuation.resume(userState.user.latestBookmarkList)
|
||||||
continuation.resume(userState.user.latestBookmarkList)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -1337,7 +1328,7 @@ class Account(
|
|||||||
zappedNote?.isZappedBy(userProfile(), this, onWasZapped)
|
zappedNote?.isZappedBy(userProfile(), this, onWasZapped)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun calculateZappedAmount(
|
suspend fun calculateZappedAmount(
|
||||||
zappedNote: Note?,
|
zappedNote: Note?,
|
||||||
onReady: (BigDecimal) -> Unit,
|
onReady: (BigDecimal) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -25,9 +25,11 @@ import androidx.compose.runtime.Stable
|
|||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MediatorLiveData
|
import androidx.lifecycle.MediatorLiveData
|
||||||
import androidx.lifecycle.distinctUntilChanged
|
import androidx.lifecycle.distinctUntilChanged
|
||||||
|
import com.vitorpamplona.amethyst.launchAndWaitAll
|
||||||
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
|
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
|
||||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||||
import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji
|
import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji
|
||||||
|
import com.vitorpamplona.amethyst.tryAndWait
|
||||||
import com.vitorpamplona.amethyst.ui.actions.relays.updated
|
import com.vitorpamplona.amethyst.ui.actions.relays.updated
|
||||||
import com.vitorpamplona.amethyst.ui.note.combineWith
|
import com.vitorpamplona.amethyst.ui.note.combineWith
|
||||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||||
@@ -65,13 +67,12 @@ import com.vitorpamplona.quartz.events.WrappedEvent
|
|||||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
import com.vitorpamplona.quartz.utils.containsAny
|
import com.vitorpamplona.quartz.utils.containsAny
|
||||||
|
import kotlinx.coroutines.CancellableContinuation
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
@@ -423,31 +424,36 @@ open class Note(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun recursiveIsPaidByCalculation(
|
private suspend fun isPaidByCalculation(
|
||||||
account: Account,
|
account: Account,
|
||||||
remainingZapPayments: List<Pair<Note, Note?>>,
|
zapEvents: List<Pair<Note, Note?>>,
|
||||||
onWasZappedByAuthor: () -> Unit,
|
onWasZappedByAuthor: () -> Unit,
|
||||||
) {
|
) {
|
||||||
if (remainingZapPayments.isEmpty()) {
|
if (zapEvents.isEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val next = remainingZapPayments.first()
|
var hasSentOne = false
|
||||||
|
|
||||||
val zapResponseEvent = next.second?.event as? LnZapPaymentResponseEvent
|
launchAndWaitAll(zapEvents) { next ->
|
||||||
if (zapResponseEvent != null) {
|
val zapResponseEvent = next.second?.event as? LnZapPaymentResponseEvent
|
||||||
account.decryptZapPaymentResponseEvent(zapResponseEvent) { response ->
|
|
||||||
if (
|
if (zapResponseEvent != null) {
|
||||||
response is PayInvoiceSuccessResponse &&
|
val result =
|
||||||
account.isNIP47Author(zapResponseEvent.requestAuthor())
|
tryAndWait { continuation ->
|
||||||
) {
|
account.decryptZapPaymentResponseEvent(zapResponseEvent) { response ->
|
||||||
|
if (
|
||||||
|
response is PayInvoiceSuccessResponse &&
|
||||||
|
account.isNIP47Author(zapResponseEvent.requestAuthor())
|
||||||
|
) {
|
||||||
|
continuation.resume(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasSentOne && result == true) {
|
||||||
|
hasSentOne = true
|
||||||
onWasZappedByAuthor()
|
onWasZappedByAuthor()
|
||||||
} else {
|
|
||||||
recursiveIsPaidByCalculation(
|
|
||||||
account,
|
|
||||||
remainingZapPayments.minus(next),
|
|
||||||
onWasZappedByAuthor,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -457,14 +463,14 @@ open class Note(
|
|||||||
option: Int?,
|
option: Int?,
|
||||||
user: User,
|
user: User,
|
||||||
account: Account,
|
account: Account,
|
||||||
remainingZapEvents: Map<Note, Note?>,
|
zapEvents: Map<Note, Note?>,
|
||||||
onWasZappedByAuthor: () -> Unit,
|
onWasZappedByAuthor: () -> Unit,
|
||||||
) {
|
) {
|
||||||
if (remainingZapEvents.isEmpty()) {
|
if (zapEvents.isEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
remainingZapEvents.forEach { next ->
|
zapEvents.forEach { next ->
|
||||||
val zapRequest = next.key.event as LnZapRequestEvent
|
val zapRequest = next.key.event as LnZapRequestEvent
|
||||||
val zapEvent = next.value?.event as? LnZapEvent
|
val zapEvent = next.value?.event as? LnZapEvent
|
||||||
|
|
||||||
@@ -487,11 +493,9 @@ open class Note(
|
|||||||
} else {
|
} else {
|
||||||
if (account.isWriteable()) {
|
if (account.isWriteable()) {
|
||||||
val result =
|
val result =
|
||||||
withTimeoutOrNull(1000) {
|
tryAndWait { continuation ->
|
||||||
suspendCancellableCoroutine { continuation ->
|
zapRequest.decryptPrivateZap(account.signer) {
|
||||||
zapRequest.decryptPrivateZap(account.signer) {
|
continuation.resume(it)
|
||||||
continuation.resume(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,7 +516,7 @@ open class Note(
|
|||||||
) {
|
) {
|
||||||
isZappedByCalculation(null, user, account, zaps, onWasZappedByAuthor)
|
isZappedByCalculation(null, user, account, zaps, onWasZappedByAuthor)
|
||||||
if (account.userProfile() == user) {
|
if (account.userProfile() == user) {
|
||||||
recursiveIsPaidByCalculation(account, zapPayments.toList(), onWasZappedByAuthor)
|
isPaidByCalculation(account, zapPayments.toList(), onWasZappedByAuthor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -564,23 +568,79 @@ open class Note(
|
|||||||
zapsAmount = sumOfAmounts
|
zapsAmount = sumOfAmounts
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun recursiveZappedAmountCalculation(
|
private suspend fun zappedAmountCalculation(
|
||||||
invoiceSet: LinkedHashSet<String>,
|
startAmount: BigDecimal,
|
||||||
remainingZapPayments: List<Pair<Note, Note?>>,
|
paidInvoiceSet: LinkedHashSet<String>,
|
||||||
|
zapPayments: List<Pair<Note, Note?>>,
|
||||||
signer: NostrSigner,
|
signer: NostrSigner,
|
||||||
output: BigDecimal,
|
|
||||||
onReady: (BigDecimal) -> Unit,
|
onReady: (BigDecimal) -> Unit,
|
||||||
) {
|
) {
|
||||||
if (remainingZapPayments.isEmpty()) {
|
if (zapPayments.isEmpty()) {
|
||||||
onReady(output)
|
onReady(startAmount)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val next = remainingZapPayments.first()
|
var output: BigDecimal = startAmount
|
||||||
|
|
||||||
(next.second?.event as? LnZapPaymentResponseEvent)?.response(signer) { noteEvent ->
|
launchAndWaitAll(zapPayments) { next ->
|
||||||
|
val result =
|
||||||
|
tryAndWait { continuation ->
|
||||||
|
processZapAmountFromResponse(
|
||||||
|
next.first,
|
||||||
|
next.second,
|
||||||
|
continuation,
|
||||||
|
signer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result != null && !paidInvoiceSet.contains(result.invoice)) {
|
||||||
|
paidInvoiceSet.add(result.invoice)
|
||||||
|
output = output.add(result.amount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onReady(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processZapAmountFromResponse(
|
||||||
|
paymentRequest: Note,
|
||||||
|
paymentResponse: Note?,
|
||||||
|
continuation: CancellableContinuation<InvoiceAmount?>,
|
||||||
|
signer: NostrSigner,
|
||||||
|
) {
|
||||||
|
val nwcRequest = paymentRequest.event as? LnZapPaymentRequestEvent
|
||||||
|
val nwcResponse = paymentResponse?.event as? LnZapPaymentResponseEvent
|
||||||
|
|
||||||
|
if (nwcRequest != null && nwcResponse != null) {
|
||||||
|
processZapAmountFromResponse(
|
||||||
|
nwcRequest,
|
||||||
|
nwcResponse,
|
||||||
|
continuation,
|
||||||
|
signer,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
continuation.resume(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InvoiceAmount(
|
||||||
|
val invoice: String,
|
||||||
|
val amount: BigDecimal,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun processZapAmountFromResponse(
|
||||||
|
nwcRequest: LnZapPaymentRequestEvent,
|
||||||
|
nwcResponse: LnZapPaymentResponseEvent,
|
||||||
|
continuation: CancellableContinuation<InvoiceAmount?>,
|
||||||
|
signer: NostrSigner,
|
||||||
|
) {
|
||||||
|
// if we can decrypt the reply
|
||||||
|
nwcResponse.response(signer) { noteEvent ->
|
||||||
|
// if it is a sucess
|
||||||
if (noteEvent is PayInvoiceSuccessResponse) {
|
if (noteEvent is PayInvoiceSuccessResponse) {
|
||||||
(next.first.event as? LnZapPaymentRequestEvent)?.lnInvoice(signer) { invoice ->
|
// if we can decrypt the invoice
|
||||||
|
nwcRequest.lnInvoice(signer) { invoice ->
|
||||||
|
// if we can parse the amount
|
||||||
val amount =
|
val amount =
|
||||||
try {
|
try {
|
||||||
LnInvoiceUtil.getAmountInSats(invoice)
|
LnInvoiceUtil.getAmountInSats(invoice)
|
||||||
@@ -589,26 +649,20 @@ open class Note(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
var newAmount = output
|
// avoid double counting
|
||||||
|
if (amount != null) {
|
||||||
if (amount != null && !invoiceSet.contains(invoice)) {
|
continuation.resume(InvoiceAmount(invoice, amount))
|
||||||
invoiceSet.add(invoice)
|
} else {
|
||||||
newAmount += amount
|
continuation.resume(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
recursiveZappedAmountCalculation(
|
|
||||||
invoiceSet,
|
|
||||||
remainingZapPayments.minus(next),
|
|
||||||
signer,
|
|
||||||
newAmount,
|
|
||||||
onReady,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
continuation.resume(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun zappedAmountWithNWCPayments(
|
suspend fun zappedAmountWithNWCPayments(
|
||||||
signer: NostrSigner,
|
signer: NostrSigner,
|
||||||
onReady: (BigDecimal) -> Unit,
|
onReady: (BigDecimal) -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -619,11 +673,11 @@ open class Note(
|
|||||||
val invoiceSet = LinkedHashSet<String>(zaps.size + zapPayments.size)
|
val invoiceSet = LinkedHashSet<String>(zaps.size + zapPayments.size)
|
||||||
zaps.forEach { (it.value?.event as? LnZapEvent)?.lnInvoice()?.let { invoiceSet.add(it) } }
|
zaps.forEach { (it.value?.event as? LnZapEvent)?.lnInvoice()?.let { invoiceSet.add(it) } }
|
||||||
|
|
||||||
recursiveZappedAmountCalculation(
|
zappedAmountCalculation(
|
||||||
|
zapsAmount,
|
||||||
invoiceSet,
|
invoiceSet,
|
||||||
zapPayments.toList(),
|
zapPayments.toList(),
|
||||||
signer,
|
signer,
|
||||||
zapsAmount,
|
|
||||||
onReady,
|
onReady,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,11 +32,10 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
|||||||
import com.vitorpamplona.amethyst.BuildConfig
|
import com.vitorpamplona.amethyst.BuildConfig
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.tryAndWait
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
@@ -304,10 +303,8 @@ class Nip96Uploader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun nip98Header(url: String): String? =
|
suspend fun nip98Header(url: String): String? =
|
||||||
withTimeoutOrNull(5000) {
|
tryAndWait { continuation ->
|
||||||
suspendCancellableCoroutine { continuation ->
|
nip98Header(url, "POST") { authorizationToken -> continuation.resume(authorizationToken) }
|
||||||
nip98Header(url, "POST") { authorizationToken -> continuation.resume(authorizationToken) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun nip98Header(
|
fun nip98Header(
|
||||||
|
|||||||
@@ -23,13 +23,12 @@ package com.vitorpamplona.amethyst.service
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.collectSuccessfulOperations
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource.user
|
|
||||||
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.collectSuccessfulSigningOperations
|
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent
|
||||||
import com.vitorpamplona.quartz.events.AppDefinitionEvent
|
import com.vitorpamplona.quartz.events.AppDefinitionEvent
|
||||||
@@ -195,8 +194,8 @@ class ZapPaymentHandler(
|
|||||||
)?.readRelays()
|
)?.readRelays()
|
||||||
}?.toSet()
|
}?.toSet()
|
||||||
|
|
||||||
collectSuccessfulSigningOperations<ZapSplitSetup, SignAllZapRequestsReturn>(
|
collectSuccessfulOperations<ZapSplitSetup, SignAllZapRequestsReturn>(
|
||||||
operationsInput = zapsToSend,
|
items = zapsToSend,
|
||||||
runRequestFor = { next: ZapSplitSetup, onReady ->
|
runRequestFor = { next: ZapSplitSetup, onReady ->
|
||||||
if (next.isLnAddress) {
|
if (next.isLnAddress) {
|
||||||
prepareZapRequestIfNeeded(note, pollOption, message, zapType) { zapRequestJson ->
|
prepareZapRequestIfNeeded(note, pollOption, message, zapType) { zapRequestJson ->
|
||||||
@@ -239,8 +238,8 @@ class ZapPaymentHandler(
|
|||||||
var progressAllPayments = 0.00f
|
var progressAllPayments = 0.00f
|
||||||
val totalWeight = invoices.sumOf { it.first.weight }
|
val totalWeight = invoices.sumOf { it.first.weight }
|
||||||
|
|
||||||
collectSuccessfulSigningOperations<Pair<ZapSplitSetup, SignAllZapRequestsReturn>, AssembleInvoiceReturn>(
|
collectSuccessfulOperations<Pair<ZapSplitSetup, SignAllZapRequestsReturn>, AssembleInvoiceReturn>(
|
||||||
operationsInput = invoices,
|
items = invoices,
|
||||||
runRequestFor = { splitZapRequestPair: Pair<ZapSplitSetup, SignAllZapRequestsReturn>, onReady ->
|
runRequestFor = { splitZapRequestPair: Pair<ZapSplitSetup, SignAllZapRequestsReturn>, onReady ->
|
||||||
assembleInvoice(
|
assembleInvoice(
|
||||||
splitSetup = splitZapRequestPair.first,
|
splitSetup = splitZapRequestPair.first,
|
||||||
@@ -272,8 +271,8 @@ class ZapPaymentHandler(
|
|||||||
) {
|
) {
|
||||||
var progressAllPayments = 0.00f
|
var progressAllPayments = 0.00f
|
||||||
|
|
||||||
collectSuccessfulSigningOperations<String, Boolean>(
|
collectSuccessfulOperations<String, Boolean>(
|
||||||
operationsInput = invoices,
|
items = invoices,
|
||||||
runRequestFor = { invoice: String, onReady ->
|
runRequestFor = { invoice: String, onReady ->
|
||||||
account.sendZapPaymentRequestFor(
|
account.sendZapPaymentRequestFor(
|
||||||
bolt11 = invoice,
|
bolt11 = invoice,
|
||||||
|
|||||||
+18
-33
@@ -25,18 +25,15 @@ import com.vitorpamplona.amethyst.AccountInfo
|
|||||||
import com.vitorpamplona.amethyst.Amethyst
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
import com.vitorpamplona.amethyst.BuildConfig
|
import com.vitorpamplona.amethyst.BuildConfig
|
||||||
import com.vitorpamplona.amethyst.LocalPreferences
|
import com.vitorpamplona.amethyst.LocalPreferences
|
||||||
|
import com.vitorpamplona.amethyst.launchAndWaitAll
|
||||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||||
|
import com.vitorpamplona.amethyst.tryAndWait
|
||||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||||
import com.vitorpamplona.quartz.events.RelayAuthEvent
|
import com.vitorpamplona.quartz.events.RelayAuthEvent
|
||||||
import com.vitorpamplona.quartz.signers.NostrSignerExternal
|
import com.vitorpamplona.quartz.signers.NostrSignerExternal
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.joinAll
|
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
@@ -63,38 +60,26 @@ class RegisterAccounts(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
coroutineScope {
|
launchAndWaitAll(remainingTos) { accountRelayPair ->
|
||||||
val jobs =
|
val result =
|
||||||
remainingTos.map { accountRelayPair ->
|
tryAndWait { continuation ->
|
||||||
async {
|
val signer = accountRelayPair.first.createSigner()
|
||||||
val result =
|
// TODO: Modify the external launcher to launch as different users.
|
||||||
withTimeoutOrNull(10000) {
|
// Right now it only registers if Amber has already approved this signature
|
||||||
suspendCancellableCoroutine { continuation ->
|
if (signer is NostrSignerExternal) {
|
||||||
val signer = accountRelayPair.first.createSigner()
|
signer.launcher.registerLauncher(
|
||||||
// TODO: Modify the external launcher to launch as different users.
|
launcher = { },
|
||||||
// Right now it only registers if Amber has already approved this signature
|
contentResolver = Amethyst.instance::contentResolverFn,
|
||||||
if (signer is NostrSignerExternal) {
|
)
|
||||||
signer.launcher.registerLauncher(
|
}
|
||||||
launcher = { },
|
|
||||||
contentResolver = Amethyst.instance::contentResolverFn,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
RelayAuthEvent.create(accountRelayPair.second, notificationToken, signer) { result ->
|
RelayAuthEvent.create(accountRelayPair.second, notificationToken, signer) { result ->
|
||||||
continuation.resume(result)
|
continuation.resume(result)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result != null) {
|
|
||||||
output.add(result)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// runs in parallel to avoid overcrowding Amber.
|
if (result != null) {
|
||||||
withTimeoutOrNull(15000) {
|
output.add(result)
|
||||||
jobs.joinAll()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-46
@@ -36,6 +36,7 @@ import coil3.imageLoader
|
|||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
import com.vitorpamplona.amethyst.Amethyst
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.collectSuccessfulOperations
|
||||||
import com.vitorpamplona.amethyst.commons.compose.GenericBaseCache
|
import com.vitorpamplona.amethyst.commons.compose.GenericBaseCache
|
||||||
import com.vitorpamplona.amethyst.commons.compose.GenericBaseCacheAsync
|
import com.vitorpamplona.amethyst.commons.compose.GenericBaseCacheAsync
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
@@ -108,9 +109,7 @@ import kotlinx.coroutines.CancellationException
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
@@ -121,12 +120,8 @@ import kotlinx.coroutines.flow.flatMapLatest
|
|||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.joinAll
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
|
|
||||||
@Immutable open class ToastMsg
|
@Immutable open class ToastMsg
|
||||||
|
|
||||||
@@ -524,8 +519,8 @@ class AccountViewModel(
|
|||||||
)
|
)
|
||||||
}.toMutableMap()
|
}.toMutableMap()
|
||||||
|
|
||||||
collectSuccessfulSigningOperations<CombinedZap, ZapAmountCommentNotification>(
|
collectSuccessfulOperations<CombinedZap, ZapAmountCommentNotification>(
|
||||||
operationsInput = zaps.filter { (it.request.event as? LnZapRequestEvent)?.isPrivateZap() == true },
|
items = zaps.filter { (it.request.event as? LnZapRequestEvent)?.isPrivateZap() == true },
|
||||||
runRequestFor = { next, onReady ->
|
runRequestFor = { next, onReady ->
|
||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
|
|
||||||
@@ -628,8 +623,8 @@ class AccountViewModel(
|
|||||||
)
|
)
|
||||||
}.toMutableMap()
|
}.toMutableMap()
|
||||||
|
|
||||||
collectSuccessfulSigningOperations<Pair<Note, Note?>, ZapAmountCommentNotification>(
|
collectSuccessfulOperations<Pair<Note, Note?>, ZapAmountCommentNotification>(
|
||||||
operationsInput = myList,
|
items = myList,
|
||||||
runRequestFor = { next, onReady ->
|
runRequestFor = { next, onReady ->
|
||||||
innerDecryptAmountMessage(next.first, next.second, onReady)
|
innerDecryptAmountMessage(next.first, next.second, onReady)
|
||||||
},
|
},
|
||||||
@@ -1679,42 +1674,6 @@ class AccountViewModel(
|
|||||||
val nip19: Nip19Bech32.ParseReturn,
|
val nip19: Nip19Bech32.ParseReturn,
|
||||||
)
|
)
|
||||||
|
|
||||||
public suspend fun <T, K> collectSuccessfulSigningOperations(
|
|
||||||
operationsInput: List<T>,
|
|
||||||
runRequestFor: (T, (K) -> Unit) -> Unit,
|
|
||||||
output: MutableMap<T, K> = mutableMapOf(),
|
|
||||||
onReady: suspend (MutableMap<T, K>) -> Unit,
|
|
||||||
) {
|
|
||||||
if (operationsInput.isEmpty()) {
|
|
||||||
onReady(output)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
coroutineScope {
|
|
||||||
val jobs =
|
|
||||||
operationsInput.map {
|
|
||||||
async {
|
|
||||||
val result =
|
|
||||||
withTimeoutOrNull(10000) {
|
|
||||||
suspendCancellableCoroutine { continuation ->
|
|
||||||
runRequestFor(it) { result: K -> continuation.resume(result) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (result != null) {
|
|
||||||
output[it] = result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// runs in parallel to avoid overcrowding Amber.
|
|
||||||
withTimeoutOrNull(15000) {
|
|
||||||
jobs.joinAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onReady(output)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun mockAccountViewModel(): AccountViewModel {
|
fun mockAccountViewModel(): AccountViewModel {
|
||||||
val sharedPreferencesViewModel: SharedPreferencesViewModel = viewModel()
|
val sharedPreferencesViewModel: SharedPreferencesViewModel = viewModel()
|
||||||
|
|||||||
Reference in New Issue
Block a user