From 07a92cd70d0e357da050c5e95e4a9ad1acc6c280 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 30 Nov 2023 10:15:36 -0500 Subject: [PATCH] Fixes all or nothing decryption of Zaps failing in a few cases. Now it has a fallback when decryption fails. --- .../amethyst/ui/note/ReactionsRow.kt | 4 +- .../ui/screen/loggedIn/AccountViewModel.kt | 101 ++++++++++++++++-- .../quartz/events/LnZapRequestEvent.kt | 8 +- .../quartz/signers/NostrSigner.kt | 3 +- .../quartz/signers/NostrSignerExternal.kt | 5 +- .../quartz/signers/NostrSignerInternal.kt | 2 +- 6 files changed, 108 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 7e08335d1..83db96829 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -453,7 +453,9 @@ private fun WatchZapAndRenderGallery( val zapsState by baseNote.live().zaps.observeAsState() var zapEvents by remember(zapsState) { - mutableStateOf>(persistentListOf()) + mutableStateOf>( + accountViewModel.cachedDecryptAmountMessageInGroup(baseNote) + ) } LaunchedEffect(key1 = zapsState) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index bcf1cb2a2..dad98ede9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -71,7 +71,10 @@ import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeoutOrNull import java.util.Locale +import kotlin.coroutines.resume import kotlin.time.measureTimedValue @Immutable @@ -220,29 +223,87 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View onNewState: (ImmutableList) -> Unit ) { viewModelScope.launch(Dispatchers.IO) { - allOrNothingSigningOperations( - remainingTos = zaps, + val myList = zaps.toList() + + val initialResults = myList.associate { + it.request to ZapAmountCommentNotification( + it.request.author, + it.request.event?.content()?.ifBlank { null }, + showAmountAxis((it.response?.event as? LnZapEvent)?.amount) + ) + }.toMutableMap() + + collectSuccessfulSigningOperations( + operationsInput = myList, runRequestFor = { next, onReady -> innerDecryptAmountMessage(next.request, next.response, onReady) } ) { - onNewState(it.toImmutableList()) + it.forEach { decrypted -> + initialResults[decrypted.key.request] = decrypted.value + } + + onNewState(initialResults.values.toImmutableList()) } } } + fun cachedDecryptAmountMessageInGroup(baseNote: Note): ImmutableList { + val myList = baseNote.zaps.toList() + + return myList.map { + val request = it.first.event as? LnZapRequestEvent + if (request?.isPrivateZap() == true) { + val cachedPrivateRequest = request.cachedPrivateZap() + if (cachedPrivateRequest != null) { + ZapAmountCommentNotification( + LocalCache.getUserIfExists(cachedPrivateRequest.pubKey) ?: it.first.author, + cachedPrivateRequest.content.ifBlank { null }, + showAmountAxis((it.second?.event as? LnZapEvent)?.amount) + ) + } else { + ZapAmountCommentNotification( + it.first.author, + it.first.event?.content()?.ifBlank { null }, + showAmountAxis((it.second?.event as? LnZapEvent)?.amount) + ) + } + } else { + ZapAmountCommentNotification( + it.first.author, + it.first.event?.content()?.ifBlank { null }, + showAmountAxis((it.second?.event as? LnZapEvent)?.amount) + ) + } + }.toImmutableList() + } + fun decryptAmountMessageInGroup( baseNote: Note, onNewState: (ImmutableList) -> Unit ) { viewModelScope.launch(Dispatchers.IO) { - allOrNothingSigningOperations, ZapAmountCommentNotification>( - remainingTos = baseNote.zaps.toList(), + val myList = baseNote.zaps.toList() + + val initialResults = myList.associate { + it.first to ZapAmountCommentNotification( + it.first.author, + it.first.event?.content()?.ifBlank { null }, + showAmountAxis((it.second?.event as? LnZapEvent)?.amount) + ) + }.toMutableMap() + + collectSuccessfulSigningOperations, ZapAmountCommentNotification>( + operationsInput = myList, runRequestFor = { next, onReady -> innerDecryptAmountMessage(next.first, next.second, onReady) } ) { - onNewState(it.toImmutableList()) + it.forEach { decrypted -> + initialResults[decrypted.key.first] = decrypted.value + } + + onNewState(initialResults.values.toImmutableList()) } } } @@ -999,3 +1060,31 @@ public fun allOrNothingSigningOperations( allOrNothingSigningOperations(remainingTos.minus(next), runRequestFor, output, onReady) } } + +public suspend fun collectSuccessfulSigningOperations( + operationsInput: List, + runRequestFor: (T, (K) -> Unit) -> Unit, + output: MutableMap = mutableMapOf(), + onReady: (MutableMap) -> Unit +) { + if (operationsInput.isEmpty()) { + onReady(output) + return + } + + for (input in operationsInput) { + // runs in sequence to avoid overcrowding Amber. + val result = withTimeoutOrNull(100) { + suspendCancellableCoroutine { continuation -> + runRequestFor(input) { result: K -> + continuation.resume(result) + } + } + } + if (result != null) { + output[input] = result + } + } + + onReady(output) +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapRequestEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapRequestEvent.kt index 74aa28034..cc4540aa0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapRequestEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/LnZapRequestEvent.kt @@ -28,7 +28,7 @@ class LnZapRequestEvent( ) : Event(id, pubKey, createdAt, kind, tags, content, sig) { @Transient - private var privateZapEvent: Event? = null + private var privateZapEvent: LnZapPrivateEvent? = null fun zappedPost() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] } @@ -36,7 +36,7 @@ class LnZapRequestEvent( fun isPrivateZap() = tags.any { t -> t.size >= 2 && t[0] == "anon" && t[1].isNotBlank() } - fun getPrivateZapEvent(loggedInUserPrivKey: ByteArray, pubKey: HexKey): Event? { + fun getPrivateZapEvent(loggedInUserPrivKey: ByteArray, pubKey: HexKey): LnZapPrivateEvent? { val anonTag = tags.firstOrNull { t -> t.size >= 2 && t[0] == "anon" } if (anonTag != null) { val encnote = anonTag[1] @@ -45,7 +45,7 @@ class LnZapRequestEvent( val note = decryptPrivateZapMessage(encnote, loggedInUserPrivKey, pubKey.hexToByteArray()) val decryptedEvent = fromJson(note) if (decryptedEvent.kind == 9733) { - return decryptedEvent + return decryptedEvent as LnZapPrivateEvent } } catch (e: Exception) { e.printStackTrace() @@ -55,7 +55,7 @@ class LnZapRequestEvent( return null } - fun cachedPrivateZap(): Event? { + fun cachedPrivateZap(): LnZapPrivateEvent? { return privateZapEvent } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSigner.kt b/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSigner.kt index 19fa451bc..ea469a21e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSigner.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSigner.kt @@ -3,6 +3,7 @@ package com.vitorpamplona.quartz.signers import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.EventFactory +import com.vitorpamplona.quartz.events.LnZapPrivateEvent import com.vitorpamplona.quartz.events.LnZapRequestEvent import com.vitorpamplona.quartz.events.PeopleListEvent import kotlinx.collections.immutable.ImmutableSet @@ -19,5 +20,5 @@ abstract class NostrSigner(val pubKey: HexKey) { abstract fun nip44Encrypt(decryptedContent: String, toPublicKey: HexKey, onReady: (String)-> Unit) abstract fun nip44Decrypt(encryptedContent: String, fromPublicKey: HexKey, onReady: (String)-> Unit) - abstract fun decryptZapEvent(event: LnZapRequestEvent, onReady: (Event)-> Unit) + abstract fun decryptZapEvent(event: LnZapRequestEvent, onReady: (LnZapPrivateEvent)-> Unit) } \ No newline at end of file diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerExternal.kt b/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerExternal.kt index 5e6b3eb76..59cafcc06 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerExternal.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerExternal.kt @@ -7,6 +7,7 @@ import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.encoders.toNpub import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.EventFactory +import com.vitorpamplona.quartz.events.LnZapPrivateEvent import com.vitorpamplona.quartz.events.LnZapRequestEvent class NostrSignerExternal( @@ -108,7 +109,7 @@ class NostrSignerExternal( ) } - override fun decryptZapEvent(event: LnZapRequestEvent, onReady: (Event)-> Unit) { + override fun decryptZapEvent(event: LnZapRequestEvent, onReady: (LnZapPrivateEvent)-> Unit) { return launcher.decryptZapEvent(event) { val event = try { Event.fromJson(it) @@ -116,7 +117,7 @@ class NostrSignerExternal( Log.e("NostrExternalSigner", "Unable to parse returned decrypted Zap: ${it}") null } - event?.let { + (event as? LnZapPrivateEvent)?.let { onReady(event) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerInternal.kt b/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerInternal.kt index e21d333af..bb9ce4bed 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerInternal.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/signers/NostrSignerInternal.kt @@ -155,7 +155,7 @@ class NostrSignerInternal(val keyPair: KeyPair): NostrSigner(keyPair.pubKey.toHe } } - override fun decryptZapEvent(event: LnZapRequestEvent, onReady: (Event)-> Unit) { + override fun decryptZapEvent(event: LnZapRequestEvent, onReady: (LnZapPrivateEvent)-> Unit) { if (keyPair.privKey == null) return val recipientPK = event.zappedAuthor().firstOrNull()