diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ReusableZapButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ReusableZapButton.kt new file mode 100644 index 000000000..dc9d10831 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ReusableZapButton.kt @@ -0,0 +1,327 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.components + +import android.content.Context +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ProgressIndicatorDefaults +import androidx.compose.material3.Text +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 +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.ZapPaymentHandler +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssemblerSubscription +import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.note.ObserveZapIcon +import com.vitorpamplona.amethyst.ui.note.PayViaIntentDialog +import com.vitorpamplona.amethyst.ui.note.ZapAmountChoicePopup +import com.vitorpamplona.amethyst.ui.note.ZapIcon +import com.vitorpamplona.amethyst.ui.note.ZappedIcon +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.ModifierWidth3dp +import com.vitorpamplona.amethyst.ui.theme.Size14Modifier +import com.vitorpamplona.amethyst.ui.theme.Size20Modifier +import com.vitorpamplona.amethyst.ui.theme.Size35dp +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +/** + * Configuration for zap button behavior and appearance + */ +data class ZapButtonConfig( + val grayTint: Color = Color.Gray, + val iconSize: Dp = Size35dp, + val iconSizeModifier: Modifier = Size20Modifier, + val animationModifier: Modifier = Size14Modifier, + val showUserFinderSubscription: Boolean = false, + val zapAmountChoices: List? = null, + val thankYouText: String? = null, + val buttonText: String? = null, +) + +/** + * Callbacks for zap button events + */ +data class ZapButtonCallbacks( + val onZapComplete: ((Boolean) -> Unit)? = null, +) + +@Composable +fun ReusableZapButton( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, + config: ZapButtonConfig = ZapButtonConfig(), + callbacks: ZapButtonCallbacks = ZapButtonCallbacks(), +) { + var wantsToZap by remember { mutableStateOf?>(null) } + var wantsToPay by remember(baseNote) { + mutableStateOf>(persistentListOf()) + } + + // Makes sure the user is loaded to get his ln address ahead of time (for DVM buttons) + if (config.showUserFinderSubscription) { + baseNote.author?.let { author -> + UserFinderFilterAssemblerSubscription(author, accountViewModel) + } + } + + val context = LocalContext.current + val scope = rememberCoroutineScope() + + var zappingProgress by remember { mutableFloatStateOf(0f) } + var zapStartingTime by remember { mutableLongStateOf(0L) } + var hasZapped by remember { mutableStateOf(false) } + + Button( + onClick = { + handleZapClick( + baseNote = baseNote, + accountViewModel = accountViewModel, + context = context, + zapAmountChoices = config.zapAmountChoices, + onZapStarts = { zapStartingTime = TimeUtils.now() }, + onZappingProgress = { progress -> + scope.launch { zappingProgress = progress } + }, + onMultipleChoices = { options -> + wantsToZap = options.toImmutableList() + }, + onError = { _, message, toUser -> + scope.launch { + zappingProgress = 0f + accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, message, toUser) + } + }, + onPayViaIntent = { wantsToPay = it }, + ) + }, + modifier = Modifier.fillMaxWidth(), + ) { + wantsToZap?.let { zapAmountChoices -> + ZapAmountChoicePopup( + baseNote = baseNote, + zapAmountChoices = zapAmountChoices, + popupYOffset = config.iconSize, + accountViewModel = accountViewModel, + onZapStarts = { zapStartingTime = TimeUtils.now() }, + onDismiss = { + wantsToZap = null + zappingProgress = 0f + }, + onChangeAmount = { + wantsToZap = null + }, + onError = { _, message, user -> + scope.launch { + zappingProgress = 0f + accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, message, user) + } + }, + onProgress = { + scope.launch(Dispatchers.Main) { zappingProgress = it } + }, + onPayViaIntent = { wantsToPay = it }, + ) + } + + if (wantsToPay.isNotEmpty()) { + PayViaIntentDialog( + payingInvoices = wantsToPay, + accountViewModel = accountViewModel, + onClose = { wantsToPay = persistentListOf() }, + onError = { + wantsToPay = persistentListOf() + scope.launch { + zappingProgress = 0f + accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, it) + } + }, + justShowError = { + scope.launch { + accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, it) + } + }, + ) + } + + // Zap Icon and Progress + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = config.iconSizeModifier, + ) { + if (zappingProgress > 0.00001 && zappingProgress < 0.99999) { + Spacer(ModifierWidth3dp) + + 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(config.iconSizeModifier) + } else { + CircularProgressIndicator( + progress = { animatedProgress }, + modifier = config.animationModifier, + strokeWidth = 2.dp, + color = config.grayTint, + ) + } + } + } + } else { + ObserveZapIcon( + baseNote, + accountViewModel, + ) { wasZappedByLoggedInUser -> + LaunchedEffect(wasZappedByLoggedInUser.value) { + hasZapped = wasZappedByLoggedInUser.value + callbacks.onZapComplete?.invoke(wasZappedByLoggedInUser.value) + + if (wasZappedByLoggedInUser.value && !accountViewModel.account.hasDonatedInThisVersion()) { + delay(1000) + accountViewModel.markDonatedInThisVersion() + } + } + + CrossfadeIfEnabled( + targetState = wasZappedByLoggedInUser.value, + label = "ZapIcon", + accountViewModel = accountViewModel, + ) { + if (it) { + ZappedIcon(config.iconSizeModifier) + } else { + ZapIcon(config.iconSizeModifier, config.grayTint) + } + } + } + } + } + + val displayText = + when { + hasZapped -> config.thankYouText ?: stringRes(id = R.string.thank_you) + config.buttonText != null -> config.buttonText + else -> stringRes(id = R.string.donate_now) + } + + Text(text = displayText) + } +} + +private fun handleZapClick( + baseNote: Note, + accountViewModel: AccountViewModel, + context: Context, + zapAmountChoices: List?, + onZapStarts: () -> Unit, + onZappingProgress: (Float) -> Unit, + onMultipleChoices: (List) -> Unit, + onError: (String, String, User?) -> Unit, + onPayViaIntent: (ImmutableList) -> Unit, +) { + if (baseNote.isDraft()) { + accountViewModel.toastManager.toast( + R.string.draft_note, + R.string.it_s_not_possible_to_zap_to_a_draft_note, + ) + return + } + + val choices = zapAmountChoices ?: accountViewModel.zapAmountChoices() + + if (choices.isEmpty()) { + accountViewModel.toastManager.toast( + stringRes(context, R.string.error_dialog_zap_error), + stringRes(context, R.string.no_zap_amount_setup_long_press_to_change), + ) + } else if (!accountViewModel.isWriteable()) { + accountViewModel.toastManager.toast( + stringRes(context, R.string.error_dialog_zap_error), + stringRes(context, R.string.login_with_a_private_key_to_be_able_to_send_zaps), + ) + } else if (choices.size == 1) { + val amount = choices.first() + + if (amount > 1100 || zapAmountChoices != null) { + onZapStarts() + accountViewModel.zap( + baseNote, + amount * 1000, + null, + "", + context, + showErrorIfNoLnAddress = false, + onError = onError, + onProgress = { onZappingProgress(it) }, + onPayViaIntent = onPayViaIntent, + ) + } else { + onMultipleChoices(listOf(1000L, 5_000L, 10_000L)) + } + } else { + if (choices.any { it > 1100 } || zapAmountChoices != null) { + onMultipleChoices(choices) + } else { + onMultipleChoices(listOf(1000L, 5_000L, 10_000L)) + } + } +} 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 7fd70dcc1..70c5afa7c 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 @@ -20,84 +20,54 @@ */ package com.vitorpamplona.amethyst.ui.note.elements -import android.content.Context -import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material3.Button import androidx.compose.material3.Card -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ProgressIndicatorDefaults import androidx.compose.material3.Text 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 -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.LinkAnnotation import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withLink import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote -import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.components.LoadNote +import com.vitorpamplona.amethyst.ui.components.ReusableZapButton +import com.vitorpamplona.amethyst.ui.components.ZapButtonConfig import com.vitorpamplona.amethyst.ui.components.appendLink import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.CloseIcon -import com.vitorpamplona.amethyst.ui.note.ObserveZapIcon -import com.vitorpamplona.amethyst.ui.note.PayViaIntentDialog -import com.vitorpamplona.amethyst.ui.note.ZapAmountChoicePopup -import com.vitorpamplona.amethyst.ui.note.ZapIcon -import com.vitorpamplona.amethyst.ui.note.ZappedIcon import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.DisplayZapSplits import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel 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 import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @Preview @@ -286,214 +256,17 @@ fun ZapDonationButton( baseNote: Note, grayTint: Color, accountViewModel: AccountViewModel, - iconSize: Dp = Size35dp, - iconSizeModifier: Modifier = Size20Modifier, - animationModifier: Modifier = Size14Modifier, nav: INav, ) { - var wantsToZap by remember { mutableStateOf?>(null) } - var wantsToPay by - remember(baseNote) { - mutableStateOf>( - persistentListOf(), - ) - } + val config = + ZapButtonConfig( + grayTint = grayTint, + ) - val context = LocalContext.current - val scope = rememberCoroutineScope() - - var zappingProgress by remember { mutableFloatStateOf(0f) } - var zapStartingTime by remember { mutableLongStateOf(0L) } - - var hasZapped by remember { mutableStateOf(false) } - - Button( - onClick = { - customZapClick( - baseNote, - accountViewModel, - context, - onZapStarts = { zapStartingTime = TimeUtils.now() }, - onZappingProgress = { progress: Float -> - scope.launch { zappingProgress = progress } - }, - onMultipleChoices = { options -> wantsToZap = options.toImmutableList() }, - onError = { _, message, toUser -> - scope.launch { - zappingProgress = 0f - accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, message, toUser) - } - }, - onPayViaIntent = { wantsToPay = it }, - ) - }, - modifier = Modifier.fillMaxWidth(), - ) { - wantsToZap?.let { - ZapAmountChoicePopup( - baseNote = baseNote, - zapAmountChoices = it, - popupYOffset = iconSize, - accountViewModel = accountViewModel, - onZapStarts = { zapStartingTime = TimeUtils.now() }, - onDismiss = { - wantsToZap = null - zappingProgress = 0f - }, - onChangeAmount = { - wantsToZap = null - }, - onError = { _, message, user -> - scope.launch { - zappingProgress = 0f - accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, message, user) - } - }, - onProgress = { - scope.launch(Dispatchers.Main) { zappingProgress = it } - }, - onPayViaIntent = { wantsToPay = it }, - ) - } - - if (wantsToPay.isNotEmpty()) { - PayViaIntentDialog( - payingInvoices = wantsToPay, - accountViewModel = accountViewModel, - onClose = { wantsToPay = persistentListOf() }, - onError = { - wantsToPay = persistentListOf() - scope.launch { - zappingProgress = 0f - accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, it) - } - }, - justShowError = { - scope.launch { - accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, it) - } - }, - ) - } - - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = iconSizeModifier, - ) { - if (zappingProgress > 0.00001 && zappingProgress < 0.99999) { - Spacer(ModifierWidth3dp) - - 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, - accountViewModel, - ) { wasZappedByLoggedInUser -> - LaunchedEffect(wasZappedByLoggedInUser.value) { - hasZapped = wasZappedByLoggedInUser.value - if (wasZappedByLoggedInUser.value && !accountViewModel.account.hasDonatedInThisVersion()) { - delay(1000) - accountViewModel.markDonatedInThisVersion() - } - } - - CrossfadeIfEnabled(targetState = wasZappedByLoggedInUser.value, label = "ZapIcon", accountViewModel = accountViewModel) { - if (it) { - ZappedIcon(iconSizeModifier) - } else { - ZapIcon(iconSizeModifier, grayTint) - } - } - } - } - } - - if (hasZapped) { - Text(text = stringRes(id = R.string.thank_you)) - } else { - Text(text = stringRes(id = R.string.donate_now)) - } - } -} - -fun customZapClick( - baseNote: Note, - accountViewModel: AccountViewModel, - context: Context, - onZapStarts: () -> Unit, - onZappingProgress: (Float) -> Unit, - onMultipleChoices: (List) -> Unit, - onError: (String, String, User?) -> Unit, - onPayViaIntent: (ImmutableList) -> Unit, -) { - if (baseNote.isDraft()) { - accountViewModel.toastManager.toast( - R.string.draft_note, - R.string.it_s_not_possible_to_zap_to_a_draft_note, - ) - return - } - - val choices = accountViewModel.zapAmountChoices() - - if (choices.isEmpty()) { - accountViewModel.toastManager.toast( - stringRes(context, R.string.error_dialog_zap_error), - stringRes(context, R.string.no_zap_amount_setup_long_press_to_change), - ) - } else if (!accountViewModel.isWriteable()) { - accountViewModel.toastManager.toast( - stringRes(context, R.string.error_dialog_zap_error), - stringRes(context, R.string.login_with_a_private_key_to_be_able_to_send_zaps), - ) - } else if (choices.size == 1) { - val amount = choices.first() - - if (amount > 1100) { - onZapStarts() - accountViewModel.zap( - baseNote, - amount * 1000, - null, - "", - context, - showErrorIfNoLnAddress = false, - onError = onError, - onProgress = { onZappingProgress(it) }, - onPayViaIntent = onPayViaIntent, - ) - } else { - onMultipleChoices(listOf(1000L, 5_000L, 10_000L)) - // recommends amounts for a monthly release. - } - } else if (choices.size > 1) { - if (choices.any { it > 1100 }) { - onMultipleChoices(choices) - } else { - onMultipleChoices(listOf(1000L, 5_000L, 10_000L)) - } - } + ReusableZapButton( + baseNote = baseNote, + accountViewModel = accountViewModel, + nav = nav, + config = config, + ) } 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 50d538d3d..80826edec 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 @@ -20,30 +20,21 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms -import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.material3.Button -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ProgressIndicatorDefaults import androidx.compose.material3.Text 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 import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.BottomStart @@ -61,25 +52,18 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.components.MyAsyncImage +import com.vitorpamplona.amethyst.ui.components.ReusableZapButton +import com.vitorpamplona.amethyst.ui.components.ZapButtonConfig import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.navigation.navs.INav -import com.vitorpamplona.amethyst.ui.note.ObserveZapIcon -import com.vitorpamplona.amethyst.ui.note.PayViaIntentDialog import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent -import com.vitorpamplona.amethyst.ui.note.ZapAmountChoicePopup -import com.vitorpamplona.amethyst.ui.note.ZapIcon -import com.vitorpamplona.amethyst.ui.note.ZappedIcon import com.vitorpamplona.amethyst.ui.note.elements.BannerImage -import com.vitorpamplona.amethyst.ui.note.elements.customZapClick import com.vitorpamplona.amethyst.ui.note.payViaIntent import com.vitorpamplona.amethyst.ui.screen.RenderFeedState import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState @@ -88,9 +72,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.DVMCard import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.dal.NIP90ContentDiscoveryFeedViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer -import com.vitorpamplona.amethyst.ui.theme.ModifierWidth3dp import com.vitorpamplona.amethyst.ui.theme.SimpleImage75Modifier -import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.quartz.lightning.LnInvoiceUtil import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceErrorResponse @@ -98,12 +80,6 @@ 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 -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch @Composable fun DvmContentDiscoveryScreen( @@ -453,160 +429,23 @@ fun ZapDVMButton( grayTint: Color, accountViewModel: AccountViewModel, iconSize: Dp = Size35dp, - iconSizeModifier: Modifier = Size20Modifier, - animationSize: Dp = 14.dp, nav: INav, ) { - val noteAuthor = baseNote.author ?: return + val config = + ZapButtonConfig( + grayTint = grayTint, + iconSize = iconSize, + showUserFinderSubscription = true, + zapAmountChoices = listOf(amount / 1000), + buttonText = "Zap ${(amount / 1000)} sats to the DVM", + ) - var wantsToZap by remember { mutableStateOf?>(null) } - var wantsToPay by - remember(baseNote) { - mutableStateOf>( - persistentListOf(), - ) - } - - // Makes sure the user is loaded to get his ln address ahead of time. - UserFinderFilterAssemblerSubscription(noteAuthor, accountViewModel) - - val context = LocalContext.current - val scope = rememberCoroutineScope() - - var zappingProgress by remember { mutableFloatStateOf(0f) } - var zapStartingTime by remember { mutableLongStateOf(0L) } - var hasZapped by remember { mutableStateOf(false) } - - Button( - onClick = { - customZapClick( - baseNote, - accountViewModel, - context, - onZapStarts = { zapStartingTime = TimeUtils.now() }, - onZappingProgress = { progress: Float -> - scope.launch { zappingProgress = progress } - }, - onMultipleChoices = { options -> wantsToZap = options }, - onError = { _, message, toUser -> - scope.launch { - zappingProgress = 0f - accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, message, toUser) - } - }, - onPayViaIntent = { wantsToPay = it }, - ) - }, - modifier = Modifier.fillMaxWidth(), - ) { - if (wantsToZap != null) { - ZapAmountChoicePopup( - baseNote = baseNote, - zapAmountChoices = persistentListOf(amount / 1000), - popupYOffset = iconSize, - accountViewModel = accountViewModel, - onZapStarts = { zapStartingTime = TimeUtils.now() }, - onDismiss = { - wantsToZap = null - zappingProgress = 0f - }, - onChangeAmount = { - wantsToZap = null - }, - onError = { _, message, user -> - scope.launch { - zappingProgress = 0f - accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, message, user) - } - }, - onProgress = { - scope.launch(Dispatchers.Main) { zappingProgress = it } - }, - onPayViaIntent = { wantsToPay = it }, - ) - } - - if (wantsToPay.isNotEmpty()) { - PayViaIntentDialog( - payingInvoices = wantsToPay, - accountViewModel = accountViewModel, - onClose = { wantsToPay = persistentListOf() }, - onError = { - wantsToPay = persistentListOf() - scope.launch { - zappingProgress = 0f - accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, it) - } - }, - justShowError = { - scope.launch { - accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, it) - } - }, - ) - } - - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = iconSizeModifier, - ) { - if (zappingProgress > 0.00001 && zappingProgress < 0.99999) { - Spacer(ModifierWidth3dp) - - 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 = remember { Modifier.size(animationSize) }, - strokeWidth = 2.dp, - color = grayTint, - ) - } - } - } - } else { - ObserveZapIcon( - baseNote, - accountViewModel, - ) { wasZappedByLoggedInUser -> - LaunchedEffect(wasZappedByLoggedInUser.value) { - hasZapped = wasZappedByLoggedInUser.value - if (wasZappedByLoggedInUser.value && !accountViewModel.account.hasDonatedInThisVersion()) { - delay(1000) - accountViewModel.markDonatedInThisVersion() - } - } - - CrossfadeIfEnabled(targetState = wasZappedByLoggedInUser.value, label = "ZapIcon", accountViewModel = accountViewModel) { - if (it) { - ZappedIcon(iconSizeModifier) - } else { - ZapIcon(iconSizeModifier, grayTint) - } - } - } - } - } - - if (hasZapped) { - Text(text = stringRes(id = R.string.thank_you)) - } else { - Text(text = "Zap " + (amount / 1000).toString() + " sats to the DVM") // stringRes(id = R.string.donate_now)) - } - } + ReusableZapButton( + baseNote = baseNote, + accountViewModel = accountViewModel, + nav = nav, + config = config, + ) } @Composable