- 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(
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)

View File

@@ -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<Pair<Note, Note?>>,
afterTimeInSeconds: Long,
account: Account,
zapEvents: List<Pair<Note, Note?>>,
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<Note, Note?>,
onWasZappedByAuthor: () -> Unit,
) {
): Boolean {
if (zapEvents.isEmpty()) {
return
return false
}
val parallelDecrypt = mutableListOf<Pair<LnZapRequestEvent, LnZapEvent?>>()
val parallelDecrypt = mutableListOf<Pair<LnZapRequestEvent, LnZapEvent>>()
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 {

View File

@@ -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<StringToastMsg?>(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,

View File

@@ -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,

View File

@@ -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<Boolean>) -> 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<Long>,
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<ZapPaymentHandler.Payable>) -> 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<Boolean>,
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,

View File

@@ -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,

View File

@@ -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<ImmutableList<Long>?>(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<Long>) -> 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,

View File

@@ -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()) {

View File

@@ -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,