- Makes zap calculation a sync routine

- Optimizes the display of the yellow zap when the zap event comes before the pay payment confirmation
- Fixes animations for a new zap over an existing zap.
This commit is contained in:
Vitor Pamplona
2025-08-07 18:02:31 -04:00
parent 8ba96ca527
commit 2315051504
9 changed files with 175 additions and 97 deletions

View File

@@ -516,10 +516,8 @@ class Account(
suspend fun calculateIfNoteWasZappedByAccount( suspend fun calculateIfNoteWasZappedByAccount(
zappedNote: Note?, zappedNote: Note?,
onWasZapped: () -> Unit, afterTimeInSeconds: Long,
) { ): Boolean = zappedNote?.isZappedBy(userProfile(), afterTimeInSeconds, this) == true
zappedNote?.isZappedBy(userProfile(), this, onWasZapped)
}
suspend fun calculateZappedAmount(zappedNote: Note): BigDecimal = zappedNote.zappedAmountWithNWCPayments(nip47SignerState) suspend fun calculateZappedAmount(zappedNote: Note): BigDecimal = zappedNote.zappedAmountWithNWCPayments(nip47SignerState)

View File

@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import coil3.util.CoilUtils.result
import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState
import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
@@ -472,28 +473,28 @@ open class Note(
} }
private suspend fun isPaidByCalculation( private suspend fun isPaidByCalculation(
zapPayments: List<Pair<Note, Note?>>,
afterTimeInSeconds: Long,
account: Account, account: Account,
zapEvents: List<Pair<Note, Note?>>, ): Boolean {
onWasZappedByAuthor: () -> Unit, if (zapPayments.isEmpty()) {
) { return false
if (zapEvents.isEmpty()) {
return
} }
var hasSentOne = false return anyAsync(zapPayments) { next ->
launchAndWaitAll(zapEvents) { next ->
val zapResponseEvent = next.second?.event as? LnZapPaymentResponseEvent val zapResponseEvent = next.second?.event as? LnZapPaymentResponseEvent
if (zapResponseEvent != null) { if (zapResponseEvent != null) {
account.nip47SignerState.decryptResponse(zapResponseEvent)?.let { response -> val response = account.nip47SignerState.decryptResponse(zapResponseEvent)
val result = response is PayInvoiceSuccessResponse && account.nip47SignerState.isNIP47Author(zapResponseEvent.requestAuthor()) if (response != null) {
response is PayInvoiceSuccessResponse &&
if (!hasSentOne && result == true) { account.nip47SignerState.isNIP47Author(zapResponseEvent.requestAuthor()) &&
hasSentOne = true zapResponseEvent.createdAt > afterTimeInSeconds
onWasZappedByAuthor() } else {
} false
} }
} else {
false
} }
} }
} }
@@ -501,75 +502,81 @@ open class Note(
private suspend fun isZappedByCalculation( private suspend fun isZappedByCalculation(
option: Int?, option: Int?,
user: User, user: User,
afterTimeInSeconds: Long,
account: Account, account: Account,
zapEvents: Map<Note, Note?>, zapEvents: Map<Note, Note?>,
onWasZappedByAuthor: () -> Unit, ): Boolean {
) {
if (zapEvents.isEmpty()) { if (zapEvents.isEmpty()) {
return return false
} }
val parallelDecrypt = mutableListOf<Pair<LnZapRequestEvent, LnZapEvent?>>() val parallelDecrypt = mutableListOf<Pair<LnZapRequestEvent, LnZapEvent>>()
zapEvents.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
if (!zapRequest.isPrivateZap()) { if (zapEvent != null) {
// public events if (!zapRequest.isPrivateZap()) {
if (zapRequest.pubKey == user.pubkeyHex && (option == null || option == zapEvent?.zappedPollOption())) { // public events
onWasZappedByAuthor() if (zapRequest.pubKey == user.pubkeyHex &&
return zapEvent.createdAt > afterTimeInSeconds &&
} (option == null || option == zapEvent.zappedPollOption())
} else { ) {
// private events return true
// 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
} }
} else { } else {
if (account.isWriteable()) { // private events
parallelDecrypt.add(Pair(zapRequest, zapEvent))
// 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 = if (parallelDecrypt.isEmpty()) {
anyAsync(parallelDecrypt) { pair -> return false
val result = account.privateZapsDecryptionCache.decryptPrivateZap(pair.first) }
result?.pubKey == user.pubkeyHex && (option == null || option == pair.second?.zappedPollOption()) return anyAsync(parallelDecrypt) { pair ->
} val result = account.privateZapsDecryptionCache.decryptPrivateZap(pair.first)
result?.pubKey == user.pubkeyHex &&
if (result) { pair.second.createdAt > afterTimeInSeconds &&
onWasZappedByAuthor() (option == null || option == pair.second.zappedPollOption())
} }
} }
suspend fun isZappedBy( suspend fun isZappedBy(
user: User, user: User,
afterTimeInSeconds: Long,
account: Account, account: Account,
onWasZappedByAuthor: () -> Unit, ): Boolean {
) { val first = isZappedByCalculation(null, user, afterTimeInSeconds, account, zaps)
isZappedByCalculation(null, user, account, zaps, onWasZappedByAuthor) if (first) return true
if (account.userProfile() == user) { if (account.userProfile() == user) {
isPaidByCalculation(account, zapPayments.toList(), onWasZappedByAuthor) return isPaidByCalculation(zapPayments.toList(), afterTimeInSeconds, account)
} }
return false
} }
suspend fun isZappedBy( suspend fun isZappedBy(
option: Int?, option: Int?,
user: User, user: User,
afterTimeInSeconds: Long,
account: Account, account: Account,
onWasZappedByAuthor: () -> Unit, ): Boolean = isZappedByCalculation(option, user, afterTimeInSeconds, account, zaps)
) {
isZappedByCalculation(option, user, account, zaps, onWasZappedByAuthor)
}
fun getReactionBy(user: User): String? = fun getReactionBy(user: User): String? =
reactions.firstNotNullOfOrNull { reactions.firstNotNullOfOrNull {

View File

@@ -52,6 +52,7 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope 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.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.Font14SP import com.vitorpamplona.amethyst.ui.theme.Font14SP
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder 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.ThemeComparisonColumn
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
import com.vitorpamplona.amethyst.ui.theme.placeholderText 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.nip02FollowList.toImmutableListOfLists
import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -517,6 +520,8 @@ fun ZapVote(
} }
var zappingProgress by remember { mutableFloatStateOf(0f) } var zappingProgress by remember { mutableFloatStateOf(0f) }
var zapStartingTime by remember { mutableLongStateOf(0L) }
var showErrorMessageDialog by remember { mutableStateOf<StringToastMsg?>(null) } var showErrorMessageDialog by remember { mutableStateOf<StringToastMsg?>(null) }
val context = LocalContext.current val context = LocalContext.current
@@ -558,6 +563,7 @@ fun ZapVote(
accountViewModel.zapAmountChoices().size == 1 && accountViewModel.zapAmountChoices().size == 1 &&
pollViewModel.isValidInputVoteAmount(accountViewModel.zapAmountChoices().first()) pollViewModel.isValidInputVoteAmount(accountViewModel.zapAmountChoices().first())
) { ) {
zapStartingTime = TimeUtils.now()
accountViewModel.zap( accountViewModel.zap(
baseNote, baseNote,
accountViewModel.zapAmountChoices().first() * 1000, accountViewModel.zapAmountChoices().first() * 1000,
@@ -583,6 +589,7 @@ fun ZapVote(
accountViewModel, accountViewModel,
pollViewModel, pollViewModel,
poolOption.option, poolOption.option,
onZapStarts = { zapStartingTime = TimeUtils.now() },
onDismiss = { onDismiss = {
wantsToZap = false wantsToZap = false
zappingProgress = 0f zappingProgress = 0f
@@ -660,9 +667,10 @@ fun ZapVote(
) )
} else { } else {
Spacer(Modifier.width(3.dp)) Spacer(Modifier.width(3.dp))
CircularProgressIndicator( CircularProgressIndicator(
progress = { zappingProgress }, progress = { zappingProgress },
modifier = Modifier.size(14.dp), modifier = Size14Modifier,
strokeWidth = 2.dp, strokeWidth = 2.dp,
) )
} }
@@ -688,6 +696,7 @@ fun FilteredZapAmountChoicePopup(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
pollViewModel: PollNoteViewModel, pollViewModel: PollNoteViewModel,
pollOption: Int, pollOption: Int,
onZapStarts: () -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
onChangeAmount: () -> Unit, onChangeAmount: () -> Unit,
onError: (title: String, text: String, toUser: User?) -> Unit, onError: (title: String, text: String, toUser: User?) -> Unit,
@@ -717,6 +726,7 @@ fun FilteredZapAmountChoicePopup(
Button( Button(
modifier = Modifier.padding(horizontal = 3.dp), modifier = Modifier.padding(horizontal = 3.dp),
onClick = { onClick = {
onZapStarts()
accountViewModel.zap( accountViewModel.zap(
baseNote, baseNote,
amountInSats * 1000, amountInSats * 1000,
@@ -743,6 +753,7 @@ fun FilteredZapAmountChoicePopup(
modifier = modifier =
Modifier.combinedClickable( Modifier.combinedClickable(
onClick = { onClick = {
onZapStarts()
accountViewModel.zap( accountViewModel.zap(
baseNote, baseNote,
amountInSats * 1000, amountInSats * 1000,

View File

@@ -103,10 +103,8 @@ class PollNoteViewModel : ViewModel() {
viewModelScope.launch(Dispatchers.Default) { viewModelScope.launch(Dispatchers.Default) {
totalZapped = totalZapped() totalZapped = totalZapped()
wasZappedByLoggedInAccount = false wasZappedByLoggedInAccount = false
account?.calculateIfNoteWasZappedByAccount(pollNote) { wasZappedByLoggedInAccount = account.calculateIfNoteWasZappedByAccount(pollNote, 0)
wasZappedByLoggedInAccount = true canZap.value = checkIfCanZap()
canZap.value = checkIfCanZap()
}
tallies.forEach { tallies.forEach {
val zappedValue = zappedPollOptionAmount(it.option) val zappedValue = zappedPollOptionAmount(it.option)
@@ -210,10 +208,8 @@ class PollNoteViewModel : ViewModel() {
suspend fun isPollOptionZappedBy( suspend fun isPollOptionZappedBy(
option: Int, option: Int,
user: User, user: User,
onWasZappedByAuthor: () -> Unit, afterTimeInSeconds: Long,
) { ): Boolean = pollNote?.isZappedBy(option, user, afterTimeInSeconds, account) == true
pollNote?.isZappedBy(option, user, account!!, onWasZappedByAuthor)
}
fun cachedIsPollOptionZappedBy( fun cachedIsPollOptionZappedBy(
option: Int, option: Int,

View File

@@ -66,6 +66,7 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember 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.reactionBox
import com.vitorpamplona.amethyst.ui.theme.ripple24dp import com.vitorpamplona.amethyst.ui.theme.ripple24dp
import com.vitorpamplona.amethyst.ui.theme.selectedReactionBoxModifier 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.nip10Notes.BaseThreadedEvent
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@@ -994,6 +997,7 @@ fun ZapReaction(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var zappingProgress by remember { mutableFloatStateOf(0f) } var zappingProgress by remember { mutableFloatStateOf(0f) }
var zapStartingTime by remember { mutableLongStateOf(0L) }
Row( Row(
verticalAlignment = CenterVertically, verticalAlignment = CenterVertically,
@@ -1008,6 +1012,7 @@ fun ZapReaction(
baseNote, baseNote,
accountViewModel, accountViewModel,
context, context,
onZapStarts = { zapStartingTime = TimeUtils.now() },
onZappingProgress = { progress: Float -> scope.launch { zappingProgress = progress } }, onZappingProgress = { progress: Float -> scope.launch { zappingProgress = progress } },
onMultipleChoices = { onMultipleChoices = {
scope.launch { scope.launch {
@@ -1033,6 +1038,7 @@ fun ZapReaction(
baseNote = baseNote, baseNote = baseNote,
popupYOffset = iconSize, popupYOffset = iconSize,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
onZapStarts = { zapStartingTime = TimeUtils.now() },
onDismiss = { onDismiss = {
wantsToZap = false wantsToZap = false
zappingProgress = 0f zappingProgress = 0f
@@ -1083,6 +1089,7 @@ fun ZapReaction(
if (wantsToSetCustomZap) { if (wantsToSetCustomZap) {
ZapCustomDialog( ZapCustomDialog(
onZapStarts = { zapStartingTime = TimeUtils.now() },
onClose = { wantsToSetCustomZap = false }, onClose = { wantsToSetCustomZap = false },
onError = { _, message, user -> onError = { _, message, user ->
scope.launch { scope.launch {
@@ -1106,11 +1113,23 @@ fun ZapReaction(
label = "ZapIconIndicator", label = "ZapIconIndicator",
) )
CircularProgressIndicator( ObserveZapIcon(
progress = { animatedProgress }, baseNote,
modifier = animationModifier, accountViewModel,
strokeWidth = 2.dp, zapStartingTime,
) ) { wasZappedByLoggedInUser ->
CrossfadeIfEnabled(targetState = wasZappedByLoggedInUser.value, label = "ZapIcon", accountViewModel = accountViewModel) {
if (it) {
ZappedIcon(iconSizeModifier)
} else {
CircularProgressIndicator(
progress = { animatedProgress },
modifier = animationModifier,
strokeWidth = 2.dp,
)
}
}
}
} else { } else {
ObserveZapIcon( ObserveZapIcon(
baseNote, baseNote,
@@ -1136,6 +1155,7 @@ fun zapClick(
baseNote: Note, baseNote: Note,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
context: Context, context: Context,
onZapStarts: () -> Unit,
onZappingProgress: (Float) -> Unit, onZappingProgress: (Float) -> Unit,
onMultipleChoices: () -> Unit, onMultipleChoices: () -> Unit,
onError: (String, String, User?) -> 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, R.string.login_with_a_private_key_to_be_able_to_send_zaps,
) )
} else if (choices.size == 1) { } else if (choices.size == 1) {
onZapStarts()
accountViewModel.zap( accountViewModel.zap(
baseNote, baseNote,
choices.first() * 1000, choices.first() * 1000,
@@ -1181,6 +1202,7 @@ fun zapClick(
fun ObserveZapIcon( fun ObserveZapIcon(
baseNote: Note, baseNote: Note,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
afterTimeInSeconds: Long = 0,
inner: @Composable (MutableState<Boolean>) -> Unit, inner: @Composable (MutableState<Boolean>) -> Unit,
) { ) {
val wasZappedByLoggedInUser = remember { mutableStateOf(false) } val wasZappedByLoggedInUser = remember { mutableStateOf(false) }
@@ -1196,10 +1218,9 @@ fun ObserveZapIcon(
LaunchedEffect(key1 = zapsState) { LaunchedEffect(key1 = zapsState) {
if (zapsState?.note?.zapPayments?.isNotEmpty() == true || zapsState?.note?.zaps?.isNotEmpty() == true) { if (zapsState?.note?.zapPayments?.isNotEmpty() == true || zapsState?.note?.zaps?.isNotEmpty() == true) {
accountViewModel.calculateIfNoteWasZappedByAccount(baseNote) { newWasZapped -> val newWasZapped = accountViewModel.calculateIfNoteWasZappedByAccount(baseNote, afterTimeInSeconds)
if (wasZappedByLoggedInUser.value != newWasZapped) { if (wasZappedByLoggedInUser.value != newWasZapped) {
wasZappedByLoggedInUser.value = newWasZapped wasZappedByLoggedInUser.value = newWasZapped
}
} }
} }
} }
@@ -1552,6 +1573,7 @@ fun ZapAmountChoicePopup(
baseNote: Note, baseNote: Note,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
popupYOffset: Dp, popupYOffset: Dp,
onZapStarts: () -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
onChangeAmount: () -> Unit, onChangeAmount: () -> Unit,
onError: (title: String, text: String, user: User?) -> Unit, onError: (title: String, text: String, user: User?) -> Unit,
@@ -1562,7 +1584,7 @@ fun ZapAmountChoicePopup(
accountViewModel.account.settings.syncedSettings.zaps.zapAmountChoices accountViewModel.account.settings.syncedSettings.zaps.zapAmountChoices
.collectAsStateWithLifecycle() .collectAsStateWithLifecycle()
ZapAmountChoicePopup(baseNote, zapAmountChoices, accountViewModel, popupYOffset, onDismiss, onChangeAmount, onError, onProgress, onPayViaIntent) ZapAmountChoicePopup(baseNote, zapAmountChoices, accountViewModel, popupYOffset, onZapStarts, onDismiss, onChangeAmount, onError, onProgress, onPayViaIntent)
} }
@Composable @Composable
@@ -1571,6 +1593,7 @@ fun ZapAmountChoicePopup(
zapAmountChoices: ImmutableList<Long>, zapAmountChoices: ImmutableList<Long>,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
popupYOffset: Dp, popupYOffset: Dp,
onZapStarts: () -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
onChangeAmount: () -> Unit, onChangeAmount: () -> Unit,
onError: (title: String, text: String, user: User?) -> Unit, onError: (title: String, text: String, user: User?) -> Unit,
@@ -1578,7 +1601,7 @@ fun ZapAmountChoicePopup(
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit, onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
) { ) {
val visibilityState = rememberVisibilityState(onDismiss) 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) @OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
@@ -1589,6 +1612,7 @@ fun ZapAmountChoicePopup(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
popupYOffset: Dp, popupYOffset: Dp,
visibilityState: MutableTransitionState<Boolean>, visibilityState: MutableTransitionState<Boolean>,
onZapStarts: () -> Unit,
onChangeAmount: () -> Unit, onChangeAmount: () -> Unit,
onError: (title: String, text: String, user: User?) -> Unit, onError: (title: String, text: String, user: User?) -> Unit,
onProgress: (percent: Float) -> Unit, onProgress: (percent: Float) -> Unit,
@@ -1615,6 +1639,7 @@ fun ZapAmountChoicePopup(
Button( Button(
modifier = Modifier.padding(horizontal = 3.dp), modifier = Modifier.padding(horizontal = 3.dp),
onClick = { onClick = {
onZapStarts()
accountViewModel.zap( accountViewModel.zap(
baseNote, baseNote,
amountInSats * 1000, amountInSats * 1000,
@@ -1641,6 +1666,7 @@ fun ZapAmountChoicePopup(
modifier = modifier =
Modifier.combinedClickable( Modifier.combinedClickable(
onClick = { onClick = {
onZapStarts()
accountViewModel.zap( accountViewModel.zap(
baseNote, baseNote,
amountInSats * 1000, amountInSats * 1000,

View File

@@ -103,6 +103,7 @@ class ZapOptionViewModel : ViewModel() {
@Composable @Composable
fun ZapCustomDialog( fun ZapCustomDialog(
onZapStarts: () -> Unit,
onClose: () -> Unit, onClose: () -> Unit,
onError: (title: String, text: String, user: User?) -> Unit, onError: (title: String, text: String, user: User?) -> Unit,
onProgress: (percent: Float) -> Unit, onProgress: (percent: Float) -> Unit,
@@ -172,6 +173,7 @@ fun ZapCustomDialog(
ZapButton( ZapButton(
isActive = postViewModel.canSend() && !baseNote.isDraft(), isActive = postViewModel.canSend() && !baseNote.isDraft(),
) { ) {
onZapStarts()
accountViewModel.zap( accountViewModel.zap(
baseNote, baseNote,
postViewModel.value()!! * 1000L, postViewModel.value()!! * 1000L,

View File

@@ -40,6 +40,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope 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.stringRes
import com.vitorpamplona.amethyst.ui.theme.ModifierWidth3dp import com.vitorpamplona.amethyst.ui.theme.ModifierWidth3dp
import com.vitorpamplona.amethyst.ui.theme.Size10dp 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.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.amethyst.ui.theme.Size35dp
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.ui.theme.imageModifier import com.vitorpamplona.amethyst.ui.theme.imageModifier
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@@ -285,7 +288,7 @@ fun ZapDonationButton(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
iconSize: Dp = Size35dp, iconSize: Dp = Size35dp,
iconSizeModifier: Modifier = Size20Modifier, iconSizeModifier: Modifier = Size20Modifier,
animationSize: Dp = 14.dp, animationModifier: Modifier = Size14Modifier,
nav: INav, nav: INav,
) { ) {
var wantsToZap by remember { mutableStateOf<ImmutableList<Long>?>(null) } var wantsToZap by remember { mutableStateOf<ImmutableList<Long>?>(null) }
@@ -300,6 +303,8 @@ fun ZapDonationButton(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var zappingProgress by remember { mutableFloatStateOf(0f) } var zappingProgress by remember { mutableFloatStateOf(0f) }
var zapStartingTime by remember { mutableLongStateOf(0L) }
var hasZapped by remember { mutableStateOf(false) } var hasZapped by remember { mutableStateOf(false) }
Button( Button(
@@ -308,6 +313,7 @@ fun ZapDonationButton(
baseNote, baseNote,
accountViewModel, accountViewModel,
context, context,
onZapStarts = { zapStartingTime = TimeUtils.now() },
onZappingProgress = { progress: Float -> onZappingProgress = { progress: Float ->
scope.launch { zappingProgress = progress } scope.launch { zappingProgress = progress }
}, },
@@ -329,6 +335,7 @@ fun ZapDonationButton(
zapAmountChoices = it, zapAmountChoices = it,
popupYOffset = iconSize, popupYOffset = iconSize,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
onZapStarts = { zapStartingTime = TimeUtils.now() },
onDismiss = { onDismiss = {
wantsToZap = null wantsToZap = null
zappingProgress = 0f zappingProgress = 0f
@@ -376,17 +383,30 @@ fun ZapDonationButton(
if (zappingProgress > 0.00001 && zappingProgress < 0.99999) { if (zappingProgress > 0.00001 && zappingProgress < 0.99999) {
Spacer(ModifierWidth3dp) Spacer(ModifierWidth3dp)
CircularProgressIndicator( val animatedProgress by animateFloatAsState(
progress = targetValue = zappingProgress,
animateFloatAsState( animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
targetValue = zappingProgress, label = "ZapIconIndicator",
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
label = "ZapIconIndicator",
).value,
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 = animationModifier,
strokeWidth = 2.dp,
color = grayTint,
)
}
}
}
} else { } else {
ObserveZapIcon( ObserveZapIcon(
baseNote, baseNote,
@@ -423,6 +443,7 @@ fun customZapClick(
baseNote: Note, baseNote: Note,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
context: Context, context: Context,
onZapStarts: () -> Unit,
onZappingProgress: (Float) -> Unit, onZappingProgress: (Float) -> Unit,
onMultipleChoices: (List<Long>) -> Unit, onMultipleChoices: (List<Long>) -> Unit,
onError: (String, String, User?) -> Unit, onError: (String, String, User?) -> Unit,
@@ -452,6 +473,7 @@ fun customZapClick(
val amount = choices.first() val amount = choices.first()
if (amount > 1100) { if (amount > 1100) {
onZapStarts()
accountViewModel.zap( accountViewModel.zap(
baseNote, baseNote,
amount * 1000, amount * 1000,

View File

@@ -395,12 +395,11 @@ class AccountViewModel(
suspend fun calculateIfNoteWasZappedByAccount( suspend fun calculateIfNoteWasZappedByAccount(
zappedNote: Note, zappedNote: Note,
onWasZapped: (Boolean) -> Unit, afterTimeInSeconds: Long,
) { ): Boolean =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
account.calculateIfNoteWasZappedByAccount(zappedNote) { onWasZapped(true) } account.calculateIfNoteWasZappedByAccount(zappedNote, afterTimeInSeconds)
} }
}
suspend fun calculateZapAmount(zappedNote: Note): String = suspend fun calculateZapAmount(zappedNote: Note): String =
if (zappedNote.zapPayments.isNotEmpty()) { if (zappedNote.zapPayments.isNotEmpty()) {

View File

@@ -40,6 +40,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope 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.nip89AppHandlers.definition.AppMetadata
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent
import com.vitorpamplona.quartz.nip90Dvms.NIP90StatusEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90StatusEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -472,6 +474,7 @@ fun ZapDVMButton(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var zappingProgress by remember { mutableFloatStateOf(0f) } var zappingProgress by remember { mutableFloatStateOf(0f) }
var zapStartingTime by remember { mutableLongStateOf(0L) }
var hasZapped by remember { mutableStateOf(false) } var hasZapped by remember { mutableStateOf(false) }
Button( Button(
@@ -480,6 +483,7 @@ fun ZapDVMButton(
baseNote, baseNote,
accountViewModel, accountViewModel,
context, context,
onZapStarts = { zapStartingTime = TimeUtils.now() },
onZappingProgress = { progress: Float -> onZappingProgress = { progress: Float ->
scope.launch { zappingProgress = progress } scope.launch { zappingProgress = progress }
}, },
@@ -501,6 +505,7 @@ fun ZapDVMButton(
zapAmountChoices = persistentListOf(amount / 1000), zapAmountChoices = persistentListOf(amount / 1000),
popupYOffset = iconSize, popupYOffset = iconSize,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
onZapStarts = { zapStartingTime = TimeUtils.now() },
onDismiss = { onDismiss = {
wantsToZap = null wantsToZap = null
zappingProgress = 0f zappingProgress = 0f
@@ -554,12 +559,24 @@ fun ZapDVMButton(
label = "ZapIconIndicator", label = "ZapIconIndicator",
) )
CircularProgressIndicator( ObserveZapIcon(
progress = { animatedProgress }, baseNote,
modifier = remember { Modifier.size(animationSize) }, accountViewModel,
strokeWidth = 2.dp, zapStartingTime,
color = grayTint, ) { 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 { } else {
ObserveZapIcon( ObserveZapIcon(
baseNote, baseNote,