diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt index 541a4c017..a9459b21c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt @@ -130,12 +130,14 @@ class LightningAddressResolver { ) } - fun lnAddressInvoice(lnaddress: String, milliSats: Long, message: String, nostrRequest: String? = null, onSuccess: (String) -> Unit, onError: (String) -> Unit) { + fun lnAddressInvoice(lnaddress: String, milliSats: Long, message: String, nostrRequest: String? = null, onSuccess: (String) -> Unit, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit) { val mapper = jacksonObjectMapper() fetchLightningAddressJson( lnaddress, onSuccess = { lnAddressJson -> + onProgress(0.4f) + val lnurlp = try { mapper.readTree(lnAddressJson) } catch (t: Throwable) { @@ -158,6 +160,8 @@ class LightningAddressResolver { message, if (allowsNostr) nostrRequest else null, onSuccess = { + onProgress(0.6f) + val lnInvoice = try { mapper.readTree(it) } catch (t: Throwable) { @@ -169,6 +173,7 @@ class LightningAddressResolver { // Forces LN Invoice amount to be the requested amount. val invoiceAmount = LnInvoiceUtil.getAmountInSats(pr) if (invoiceAmount.multiply(BigDecimal(1000)).toLong() == BigDecimal(milliSats).toLong()) { + onProgress(0.7f) onSuccess(pr) } else { onError("Incorrect invoice amount (${invoiceAmount.toLong()} sats) from server") diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt index 63d96bb35..8cf0b80c4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/InvoiceRequest.kt @@ -149,6 +149,8 @@ fun InvoiceRequest(lud16: String, toUserPubKeyHex: String, account: Account, onC Toast.makeText(context, it, Toast.LENGTH_SHORT).show() onClose() } + }, + onProgress = { } ) }, 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 84487c0a8..cfc97c565 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 @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults +import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme @@ -33,6 +34,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter @@ -307,7 +309,10 @@ fun ZapReaction( val context = LocalContext.current val scope = rememberCoroutineScope() + var zappingProgress by remember { mutableStateOf(0f) } + Row( + verticalAlignment = CenterVertically, modifier = Modifier .then(Modifier.size(20.dp)) .combinedClickable( @@ -336,17 +341,26 @@ fun ZapReaction( .show() } } else if (account.zapAmountChoices.size == 1) { - accountViewModel.zap( - baseNote, - account.zapAmountChoices.first() * 1000, - "", - context - ) { - scope.launch { - Toast - .makeText(context, it, Toast.LENGTH_SHORT) - .show() - } + scope.launch(Dispatchers.IO) { + accountViewModel.zap( + baseNote, + account.zapAmountChoices.first() * 1000, + "", + context, + onError = { + scope.launch { + zappingProgress = 0f + Toast + .makeText(context, it, Toast.LENGTH_SHORT) + .show() + } + }, + onProgress = { + scope.launch(Dispatchers.Main) { + zappingProgress = it + } + } + ) } } else if (account.zapAmountChoices.size > 1) { wantsToZap = true @@ -363,6 +377,7 @@ fun ZapReaction( accountViewModel, onDismiss = { wantsToZap = false + zappingProgress = 0f }, onChangeAmount = { wantsToZap = false @@ -370,8 +385,14 @@ fun ZapReaction( }, onError = { scope.launch { + zappingProgress = 0f Toast.makeText(context, it, Toast.LENGTH_SHORT).show() } + }, + onProgress = { + scope.launch(Dispatchers.Main) { + zappingProgress = it + } } ) } @@ -380,6 +401,7 @@ fun ZapReaction( } if (zappedNote?.isZappedBy(account.userProfile()) == true) { + zappingProgress = 1f Icon( imageVector = Icons.Default.Bolt, contentDescription = stringResource(R.string.zaps), @@ -387,12 +409,19 @@ fun ZapReaction( tint = BitcoinOrange ) } else { - Icon( - imageVector = Icons.Outlined.Bolt, - contentDescription = stringResource(id = R.string.zaps), - modifier = Modifier.size(20.dp), - tint = grayTint - ) + if (zappingProgress < 0.1 || zappingProgress > 0.99) { + Icon( + imageVector = Icons.Outlined.Bolt, + contentDescription = stringResource(id = R.string.zaps), + modifier = Modifier.size(20.dp), + tint = grayTint + ) + } else { + CircularProgressIndicator( + progress = zappingProgress, + modifier = Modifier.size(15.dp) + ) + } } } @@ -485,12 +514,21 @@ private fun BoostTypeChoicePopup(baseNote: Note, accountViewModel: AccountViewMo @OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class) @Composable -fun ZapAmountChoicePopup(baseNote: Note, accountViewModel: AccountViewModel, onDismiss: () -> Unit, onChangeAmount: () -> Unit, onError: (text: String) -> Unit) { +fun ZapAmountChoicePopup( + baseNote: Note, + accountViewModel: AccountViewModel, + onDismiss: () -> Unit, + onChangeAmount: () -> Unit, + onError: (text: String) -> Unit, + onProgress: (percent: Float) -> Unit +) { val context = LocalContext.current val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account ?: return + val scope = rememberCoroutineScope() + Popup( alignment = Alignment.BottomCenter, offset = IntOffset(0, -50), @@ -501,8 +539,17 @@ fun ZapAmountChoicePopup(baseNote: Note, accountViewModel: AccountViewModel, onD Button( modifier = Modifier.padding(horizontal = 3.dp), onClick = { - accountViewModel.zap(baseNote, amountInSats * 1000, "", context, onError) - onDismiss() + scope.launch(Dispatchers.IO) { + accountViewModel.zap( + baseNote, + amountInSats * 1000, + "", + context, + onError, + onProgress + ) + onDismiss() + } }, shape = RoundedCornerShape(20.dp), colors = ButtonDefaults @@ -516,8 +563,17 @@ fun ZapAmountChoicePopup(baseNote: Note, accountViewModel: AccountViewModel, onD textAlign = TextAlign.Center, modifier = Modifier.combinedClickable( onClick = { - accountViewModel.zap(baseNote, amountInSats * 1000, "", context, onError) - onDismiss() + scope.launch(Dispatchers.IO) { + accountViewModel.zap( + baseNote, + amountInSats * 1000, + "", + context, + onError, + onProgress + ) + onDismiss() + } }, onLongClick = { onChangeAmount() 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 8adbb6c42..aee875037 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 @@ -7,6 +7,7 @@ import androidx.core.content.ContextCompat import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.map +import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AccountState @@ -14,6 +15,9 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver import com.vitorpamplona.amethyst.service.model.ReportEvent +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import java.util.Locale class AccountViewModel(private val account: Account) : ViewModel() { @@ -48,7 +52,7 @@ class AccountViewModel(private val account: Account) : ViewModel() { account.delete(account.boostsTo(note)) } - fun zap(note: Note, amount: Long, message: String, context: Context, onError: (String) -> Unit) { + suspend fun zap(note: Note, amount: Long, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit) { val lud16 = note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim() if (lud16.isNullOrBlank()) { @@ -58,22 +62,34 @@ class AccountViewModel(private val account: Account) : ViewModel() { val zapRequest = account.createZapRequestFor(note) + onProgress(0.10f) + LightningAddressResolver().lnAddressInvoice( lud16, amount, message, zapRequest?.toJson(), onSuccess = { + onProgress(0.7f) if (account.hasWalletConnectSetup()) { account.sendZapPaymentRequestFor(it) + onProgress(0.8f) + + // Awaits for the event to come back to LocalCache. + viewModelScope.launch(Dispatchers.IO) { + delay(1000) + onProgress(0f) + } } else { runCatching { val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$it")) ContextCompat.startActivity(context, intent, null) } + onProgress(0f) } }, - onError = onError + onError = onError, + onProgress = onProgress ) }