diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index f0c81c046..ec529464a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -516,10 +516,8 @@ class Account( suspend fun calculateIfNoteWasZappedByAccount( zappedNote: Note?, - onWasZapped: () -> Unit, - ) { - zappedNote?.isZappedBy(userProfile(), this, onWasZapped) - } + afterTimeInSeconds: Long, + ): Boolean = zappedNote?.isZappedBy(userProfile(), afterTimeInSeconds, this) == true suspend fun calculateZappedAmount(zappedNote: Note): BigDecimal = zappedNote.zappedAmountWithNWCPayments(nip47SignerState) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 03158d3ee..19c2d9b04 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.model import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable +import coil3.util.CoilUtils.result import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState import com.vitorpamplona.amethyst.service.checkNotInMainThread @@ -472,28 +473,28 @@ open class Note( } private suspend fun isPaidByCalculation( + zapPayments: List>, + afterTimeInSeconds: Long, account: Account, - zapEvents: List>, - onWasZappedByAuthor: () -> Unit, - ) { - if (zapEvents.isEmpty()) { - return + ): Boolean { + if (zapPayments.isEmpty()) { + return false } - var hasSentOne = false - - launchAndWaitAll(zapEvents) { next -> + return anyAsync(zapPayments) { next -> val zapResponseEvent = next.second?.event as? LnZapPaymentResponseEvent if (zapResponseEvent != null) { - account.nip47SignerState.decryptResponse(zapResponseEvent)?.let { response -> - val result = response is PayInvoiceSuccessResponse && account.nip47SignerState.isNIP47Author(zapResponseEvent.requestAuthor()) - - if (!hasSentOne && result == true) { - hasSentOne = true - onWasZappedByAuthor() - } + val response = account.nip47SignerState.decryptResponse(zapResponseEvent) + if (response != null) { + response is PayInvoiceSuccessResponse && + account.nip47SignerState.isNIP47Author(zapResponseEvent.requestAuthor()) && + zapResponseEvent.createdAt > afterTimeInSeconds + } else { + false } + } else { + false } } } @@ -501,75 +502,81 @@ open class Note( private suspend fun isZappedByCalculation( option: Int?, user: User, + afterTimeInSeconds: Long, account: Account, zapEvents: Map, - onWasZappedByAuthor: () -> Unit, - ) { + ): Boolean { if (zapEvents.isEmpty()) { - return + return false } - val parallelDecrypt = mutableListOf>() + val parallelDecrypt = mutableListOf>() zapEvents.forEach { next -> val zapRequest = next.key.event as LnZapRequestEvent val zapEvent = next.value?.event as? LnZapEvent - if (!zapRequest.isPrivateZap()) { - // public events - if (zapRequest.pubKey == user.pubkeyHex && (option == null || option == zapEvent?.zappedPollOption())) { - onWasZappedByAuthor() - return - } - } else { - // private events - - // if has already decrypted - val privateZap = account.privateZapsDecryptionCache.cachedPrivateZap(zapRequest) - if (privateZap != null) { - if (privateZap.pubKey == user.pubkeyHex && (option == null || option == zapEvent?.zappedPollOption())) { - onWasZappedByAuthor() - return + if (zapEvent != null) { + if (!zapRequest.isPrivateZap()) { + // public events + if (zapRequest.pubKey == user.pubkeyHex && + zapEvent.createdAt > afterTimeInSeconds && + (option == null || option == zapEvent.zappedPollOption()) + ) { + return true } } else { - if (account.isWriteable()) { - parallelDecrypt.add(Pair(zapRequest, zapEvent)) + // private events + + // if has already decrypted + val privateZap = account.privateZapsDecryptionCache.cachedPrivateZap(zapRequest) + if (privateZap != null) { + if (privateZap.pubKey == user.pubkeyHex && + zapEvent.createdAt > afterTimeInSeconds && + (option == null || option == zapEvent.zappedPollOption()) + ) { + return true + } + } else { + if (account.isWriteable()) { + parallelDecrypt.add(Pair(zapRequest, zapEvent)) + } } } } } - val result = - anyAsync(parallelDecrypt) { pair -> - val result = account.privateZapsDecryptionCache.decryptPrivateZap(pair.first) + if (parallelDecrypt.isEmpty()) { + return false + } - result?.pubKey == user.pubkeyHex && (option == null || option == pair.second?.zappedPollOption()) - } - - if (result) { - onWasZappedByAuthor() + return anyAsync(parallelDecrypt) { pair -> + val result = account.privateZapsDecryptionCache.decryptPrivateZap(pair.first) + result?.pubKey == user.pubkeyHex && + pair.second.createdAt > afterTimeInSeconds && + (option == null || option == pair.second.zappedPollOption()) } } suspend fun isZappedBy( user: User, + afterTimeInSeconds: Long, account: Account, - onWasZappedByAuthor: () -> Unit, - ) { - isZappedByCalculation(null, user, account, zaps, onWasZappedByAuthor) + ): Boolean { + val first = isZappedByCalculation(null, user, afterTimeInSeconds, account, zaps) + if (first) return true if (account.userProfile() == user) { - isPaidByCalculation(account, zapPayments.toList(), onWasZappedByAuthor) + return isPaidByCalculation(zapPayments.toList(), afterTimeInSeconds, account) } + return false } suspend fun isZappedBy( option: Int?, user: User, + afterTimeInSeconds: Long, account: Account, - onWasZappedByAuthor: () -> Unit, - ) { - isZappedByCalculation(option, user, account, zaps, onWasZappedByAuthor) - } + ): Boolean = isZappedByCalculation(option, user, afterTimeInSeconds, account, zaps) fun getReactionBy(user: User): String? = reactions.firstNotNullOfOrNull { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt index c448e0179..f7473724a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt @@ -52,6 +52,7 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -90,6 +91,7 @@ import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.Font14SP import com.vitorpamplona.amethyst.ui.theme.QuoteBorder +import com.vitorpamplona.amethyst.ui.theme.Size14Modifier import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink import com.vitorpamplona.amethyst.ui.theme.placeholderText @@ -100,6 +102,7 @@ import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers @@ -517,6 +520,8 @@ fun ZapVote( } var zappingProgress by remember { mutableFloatStateOf(0f) } + var zapStartingTime by remember { mutableLongStateOf(0L) } + var showErrorMessageDialog by remember { mutableStateOf(null) } val context = LocalContext.current @@ -558,6 +563,7 @@ fun ZapVote( accountViewModel.zapAmountChoices().size == 1 && pollViewModel.isValidInputVoteAmount(accountViewModel.zapAmountChoices().first()) ) { + zapStartingTime = TimeUtils.now() accountViewModel.zap( baseNote, accountViewModel.zapAmountChoices().first() * 1000, @@ -583,6 +589,7 @@ fun ZapVote( accountViewModel, pollViewModel, poolOption.option, + onZapStarts = { zapStartingTime = TimeUtils.now() }, onDismiss = { wantsToZap = false zappingProgress = 0f @@ -660,9 +667,10 @@ fun ZapVote( ) } else { Spacer(Modifier.width(3.dp)) + CircularProgressIndicator( progress = { zappingProgress }, - modifier = Modifier.size(14.dp), + modifier = Size14Modifier, strokeWidth = 2.dp, ) } @@ -688,6 +696,7 @@ fun FilteredZapAmountChoicePopup( accountViewModel: AccountViewModel, pollViewModel: PollNoteViewModel, pollOption: Int, + onZapStarts: () -> Unit, onDismiss: () -> Unit, onChangeAmount: () -> Unit, onError: (title: String, text: String, toUser: User?) -> Unit, @@ -717,6 +726,7 @@ fun FilteredZapAmountChoicePopup( Button( modifier = Modifier.padding(horizontal = 3.dp), onClick = { + onZapStarts() accountViewModel.zap( baseNote, amountInSats * 1000, @@ -743,6 +753,7 @@ fun FilteredZapAmountChoicePopup( modifier = Modifier.combinedClickable( onClick = { + onZapStarts() accountViewModel.zap( baseNote, amountInSats * 1000, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt index 369252068..9bee68081 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt @@ -103,10 +103,8 @@ class PollNoteViewModel : ViewModel() { viewModelScope.launch(Dispatchers.Default) { totalZapped = totalZapped() wasZappedByLoggedInAccount = false - account?.calculateIfNoteWasZappedByAccount(pollNote) { - wasZappedByLoggedInAccount = true - canZap.value = checkIfCanZap() - } + wasZappedByLoggedInAccount = account.calculateIfNoteWasZappedByAccount(pollNote, 0) + canZap.value = checkIfCanZap() tallies.forEach { val zappedValue = zappedPollOptionAmount(it.option) @@ -210,10 +208,8 @@ class PollNoteViewModel : ViewModel() { suspend fun isPollOptionZappedBy( option: Int, user: User, - onWasZappedByAuthor: () -> Unit, - ) { - pollNote?.isZappedBy(option, user, account!!, onWasZappedByAuthor) - } + afterTimeInSeconds: Long, + ): Boolean = pollNote?.isZappedBy(option, user, afterTimeInSeconds, account) == true fun cachedIsPollOptionZappedBy( option: Int, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 76927db46..aa410cad6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -66,6 +66,7 @@ import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember @@ -154,11 +155,13 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.reactionBox import com.vitorpamplona.amethyst.ui.theme.ripple24dp import com.vitorpamplona.amethyst.ui.theme.selectedReactionBoxModifier +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.EmptyClientListener.onError import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.persistentListOf @@ -994,6 +997,7 @@ fun ZapReaction( val scope = rememberCoroutineScope() var zappingProgress by remember { mutableFloatStateOf(0f) } + var zapStartingTime by remember { mutableLongStateOf(0L) } Row( verticalAlignment = CenterVertically, @@ -1008,6 +1012,7 @@ fun ZapReaction( baseNote, accountViewModel, context, + onZapStarts = { zapStartingTime = TimeUtils.now() }, onZappingProgress = { progress: Float -> scope.launch { zappingProgress = progress } }, onMultipleChoices = { scope.launch { @@ -1033,6 +1038,7 @@ fun ZapReaction( baseNote = baseNote, popupYOffset = iconSize, accountViewModel = accountViewModel, + onZapStarts = { zapStartingTime = TimeUtils.now() }, onDismiss = { wantsToZap = false zappingProgress = 0f @@ -1083,6 +1089,7 @@ fun ZapReaction( if (wantsToSetCustomZap) { ZapCustomDialog( + onZapStarts = { zapStartingTime = TimeUtils.now() }, onClose = { wantsToSetCustomZap = false }, onError = { _, message, user -> scope.launch { @@ -1106,11 +1113,23 @@ fun ZapReaction( label = "ZapIconIndicator", ) - CircularProgressIndicator( - progress = { animatedProgress }, - modifier = animationModifier, - strokeWidth = 2.dp, - ) + ObserveZapIcon( + baseNote, + accountViewModel, + zapStartingTime, + ) { wasZappedByLoggedInUser -> + CrossfadeIfEnabled(targetState = wasZappedByLoggedInUser.value, label = "ZapIcon", accountViewModel = accountViewModel) { + if (it) { + ZappedIcon(iconSizeModifier) + } else { + CircularProgressIndicator( + progress = { animatedProgress }, + modifier = animationModifier, + strokeWidth = 2.dp, + ) + } + } + } } else { ObserveZapIcon( baseNote, @@ -1136,6 +1155,7 @@ fun zapClick( baseNote: Note, accountViewModel: AccountViewModel, context: Context, + onZapStarts: () -> Unit, onZappingProgress: (Float) -> Unit, onMultipleChoices: () -> Unit, onError: (String, String, User?) -> Unit, @@ -1162,6 +1182,7 @@ fun zapClick( R.string.login_with_a_private_key_to_be_able_to_send_zaps, ) } else if (choices.size == 1) { + onZapStarts() accountViewModel.zap( baseNote, choices.first() * 1000, @@ -1181,6 +1202,7 @@ fun zapClick( fun ObserveZapIcon( baseNote: Note, accountViewModel: AccountViewModel, + afterTimeInSeconds: Long = 0, inner: @Composable (MutableState) -> Unit, ) { val wasZappedByLoggedInUser = remember { mutableStateOf(false) } @@ -1196,10 +1218,9 @@ fun ObserveZapIcon( LaunchedEffect(key1 = zapsState) { if (zapsState?.note?.zapPayments?.isNotEmpty() == true || zapsState?.note?.zaps?.isNotEmpty() == true) { - accountViewModel.calculateIfNoteWasZappedByAccount(baseNote) { newWasZapped -> - if (wasZappedByLoggedInUser.value != newWasZapped) { - wasZappedByLoggedInUser.value = newWasZapped - } + val newWasZapped = accountViewModel.calculateIfNoteWasZappedByAccount(baseNote, afterTimeInSeconds) + if (wasZappedByLoggedInUser.value != newWasZapped) { + wasZappedByLoggedInUser.value = newWasZapped } } } @@ -1552,6 +1573,7 @@ fun ZapAmountChoicePopup( baseNote: Note, accountViewModel: AccountViewModel, popupYOffset: Dp, + onZapStarts: () -> Unit, onDismiss: () -> Unit, onChangeAmount: () -> Unit, onError: (title: String, text: String, user: User?) -> Unit, @@ -1562,7 +1584,7 @@ fun ZapAmountChoicePopup( accountViewModel.account.settings.syncedSettings.zaps.zapAmountChoices .collectAsStateWithLifecycle() - ZapAmountChoicePopup(baseNote, zapAmountChoices, accountViewModel, popupYOffset, onDismiss, onChangeAmount, onError, onProgress, onPayViaIntent) + ZapAmountChoicePopup(baseNote, zapAmountChoices, accountViewModel, popupYOffset, onZapStarts, onDismiss, onChangeAmount, onError, onProgress, onPayViaIntent) } @Composable @@ -1571,6 +1593,7 @@ fun ZapAmountChoicePopup( zapAmountChoices: ImmutableList, accountViewModel: AccountViewModel, popupYOffset: Dp, + onZapStarts: () -> Unit, onDismiss: () -> Unit, onChangeAmount: () -> Unit, onError: (title: String, text: String, user: User?) -> Unit, @@ -1578,7 +1601,7 @@ fun ZapAmountChoicePopup( onPayViaIntent: (ImmutableList) -> Unit, ) { val visibilityState = rememberVisibilityState(onDismiss) - ZapAmountChoicePopup(baseNote, zapAmountChoices, accountViewModel, popupYOffset, visibilityState, onChangeAmount, onError, onProgress, onPayViaIntent) + ZapAmountChoicePopup(baseNote, zapAmountChoices, accountViewModel, popupYOffset, visibilityState, onZapStarts, onChangeAmount, onError, onProgress, onPayViaIntent) } @OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class) @@ -1589,6 +1612,7 @@ fun ZapAmountChoicePopup( accountViewModel: AccountViewModel, popupYOffset: Dp, visibilityState: MutableTransitionState, + onZapStarts: () -> Unit, onChangeAmount: () -> Unit, onError: (title: String, text: String, user: User?) -> Unit, onProgress: (percent: Float) -> Unit, @@ -1615,6 +1639,7 @@ fun ZapAmountChoicePopup( Button( modifier = Modifier.padding(horizontal = 3.dp), onClick = { + onZapStarts() accountViewModel.zap( baseNote, amountInSats * 1000, @@ -1641,6 +1666,7 @@ fun ZapAmountChoicePopup( modifier = Modifier.combinedClickable( onClick = { + onZapStarts() accountViewModel.zap( baseNote, amountInSats * 1000, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt index 0ebb39184..7526b3e9e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt @@ -103,6 +103,7 @@ class ZapOptionViewModel : ViewModel() { @Composable fun ZapCustomDialog( + onZapStarts: () -> Unit, onClose: () -> Unit, onError: (title: String, text: String, user: User?) -> Unit, onProgress: (percent: Float) -> Unit, @@ -172,6 +173,7 @@ fun ZapCustomDialog( ZapButton( isActive = postViewModel.canSend() && !baseNote.isDraft(), ) { + onZapStarts() accountViewModel.zap( baseNote, postViewModel.value()!! * 1000L, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ZapTheDevsCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ZapTheDevsCard.kt index 1826aa018..7fd70dcc1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ZapTheDevsCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ZapTheDevsCard.kt @@ -40,6 +40,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -83,12 +84,14 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.ModifierWidth3dp import com.vitorpamplona.amethyst.ui.theme.Size10dp +import com.vitorpamplona.amethyst.ui.theme.Size14Modifier import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.imageModifier import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -285,7 +288,7 @@ fun ZapDonationButton( accountViewModel: AccountViewModel, iconSize: Dp = Size35dp, iconSizeModifier: Modifier = Size20Modifier, - animationSize: Dp = 14.dp, + animationModifier: Modifier = Size14Modifier, nav: INav, ) { var wantsToZap by remember { mutableStateOf?>(null) } @@ -300,6 +303,8 @@ fun ZapDonationButton( val scope = rememberCoroutineScope() var zappingProgress by remember { mutableFloatStateOf(0f) } + var zapStartingTime by remember { mutableLongStateOf(0L) } + var hasZapped by remember { mutableStateOf(false) } Button( @@ -308,6 +313,7 @@ fun ZapDonationButton( baseNote, accountViewModel, context, + onZapStarts = { zapStartingTime = TimeUtils.now() }, onZappingProgress = { progress: Float -> scope.launch { zappingProgress = progress } }, @@ -329,6 +335,7 @@ fun ZapDonationButton( zapAmountChoices = it, popupYOffset = iconSize, accountViewModel = accountViewModel, + onZapStarts = { zapStartingTime = TimeUtils.now() }, onDismiss = { wantsToZap = null zappingProgress = 0f @@ -376,17 +383,30 @@ fun ZapDonationButton( if (zappingProgress > 0.00001 && zappingProgress < 0.99999) { Spacer(ModifierWidth3dp) - CircularProgressIndicator( - progress = - animateFloatAsState( - targetValue = zappingProgress, - animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec, - label = "ZapIconIndicator", - ).value, - modifier = remember { Modifier.size(animationSize) }, - strokeWidth = 2.dp, - color = grayTint, + val animatedProgress by animateFloatAsState( + targetValue = zappingProgress, + animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec, + label = "ZapIconIndicator", ) + + ObserveZapIcon( + baseNote, + accountViewModel, + zapStartingTime, + ) { wasZappedByLoggedInUser -> + CrossfadeIfEnabled(targetState = wasZappedByLoggedInUser.value, label = "ZapIcon", accountViewModel = accountViewModel) { + if (it) { + ZappedIcon(iconSizeModifier) + } else { + CircularProgressIndicator( + progress = { animatedProgress }, + modifier = animationModifier, + strokeWidth = 2.dp, + color = grayTint, + ) + } + } + } } else { ObserveZapIcon( baseNote, @@ -423,6 +443,7 @@ fun customZapClick( baseNote: Note, accountViewModel: AccountViewModel, context: Context, + onZapStarts: () -> Unit, onZappingProgress: (Float) -> Unit, onMultipleChoices: (List) -> Unit, onError: (String, String, User?) -> Unit, @@ -452,6 +473,7 @@ fun customZapClick( val amount = choices.first() if (amount > 1100) { + onZapStarts() accountViewModel.zap( baseNote, amount * 1000, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 4005fbe69..a40262366 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -395,12 +395,11 @@ class AccountViewModel( suspend fun calculateIfNoteWasZappedByAccount( zappedNote: Note, - onWasZapped: (Boolean) -> Unit, - ) { + afterTimeInSeconds: Long, + ): Boolean = withContext(Dispatchers.IO) { - account.calculateIfNoteWasZappedByAccount(zappedNote) { onWasZapped(true) } + account.calculateIfNoteWasZappedByAccount(zappedNote, afterTimeInSeconds) } - } suspend fun calculateZapAmount(zappedNote: Note): String = if (zappedNote.zapPayments.isNotEmpty()) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt index 1aefd5a1b..d197e8029 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt @@ -40,6 +40,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -97,6 +98,7 @@ import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppMetadata import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90StatusEvent +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers @@ -472,6 +474,7 @@ fun ZapDVMButton( val scope = rememberCoroutineScope() var zappingProgress by remember { mutableFloatStateOf(0f) } + var zapStartingTime by remember { mutableLongStateOf(0L) } var hasZapped by remember { mutableStateOf(false) } Button( @@ -480,6 +483,7 @@ fun ZapDVMButton( baseNote, accountViewModel, context, + onZapStarts = { zapStartingTime = TimeUtils.now() }, onZappingProgress = { progress: Float -> scope.launch { zappingProgress = progress } }, @@ -501,6 +505,7 @@ fun ZapDVMButton( zapAmountChoices = persistentListOf(amount / 1000), popupYOffset = iconSize, accountViewModel = accountViewModel, + onZapStarts = { zapStartingTime = TimeUtils.now() }, onDismiss = { wantsToZap = null zappingProgress = 0f @@ -554,12 +559,24 @@ fun ZapDVMButton( label = "ZapIconIndicator", ) - CircularProgressIndicator( - progress = { animatedProgress }, - modifier = remember { Modifier.size(animationSize) }, - strokeWidth = 2.dp, - color = grayTint, - ) + ObserveZapIcon( + baseNote, + accountViewModel, + zapStartingTime, + ) { wasZappedByLoggedInUser -> + CrossfadeIfEnabled(targetState = wasZappedByLoggedInUser.value, label = "ZapIcon", accountViewModel = accountViewModel) { + if (it) { + ZappedIcon(iconSizeModifier) + } else { + CircularProgressIndicator( + progress = { animatedProgress }, + modifier = remember { Modifier.size(animationSize) }, + strokeWidth = 2.dp, + color = grayTint, + ) + } + } + } } else { ObserveZapIcon( baseNote,