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 index dc9d10831..5e1dbae50 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ReusableZapButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ReusableZapButton.kt @@ -50,12 +50,14 @@ 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.components.toasts.multiline.UserBasedErrorMessage import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route 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.payViaIntent import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.ModifierWidth3dp @@ -64,11 +66,12 @@ 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 +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid /** * Configuration for zap button behavior and appearance @@ -91,6 +94,7 @@ data class ZapButtonCallbacks( val onZapComplete: ((Boolean) -> Unit)? = null, ) +@OptIn(ExperimentalUuidApi::class) @Composable fun ReusableZapButton( baseNote: Note, @@ -100,9 +104,6 @@ fun ReusableZapButton( 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) { @@ -138,7 +139,19 @@ fun ReusableZapButton( accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, message, toUser) } }, - onPayViaIntent = { wantsToPay = it }, + onPayViaIntent = { + if (it.size == 1) { + val payable = it.first() + payViaIntent(payable.invoice, context, { }) { + zappingProgress = 0f + accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, UserBasedErrorMessage(it, payable.info.user)) + } + } else { + val uid = Uuid.random().toString() + accountViewModel.tempManualPaymentCache.put(uid, it) + nav.nav(Route.ManualZapSplitPayment(uid)) + } + }, ) }, modifier = Modifier.fillMaxWidth(), @@ -166,25 +179,17 @@ fun ReusableZapButton( 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) + onPayViaIntent = { + if (it.size == 1) { + val payable = it.first() + payViaIntent(payable.invoice, context, { }) { + zappingProgress = 0f + accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, UserBasedErrorMessage(it, payable.info.user)) + } + } else { + val uid = Uuid.random().toString() + accountViewModel.tempManualPaymentCache.put(uid, it) + nav.nav(Route.ManualZapSplitPayment(uid)) } }, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index b3d2671ea..af2e26dad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -53,6 +53,7 @@ import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.routes.getRouteWithArguments import com.vitorpamplona.amethyst.ui.navigation.routes.isBaseRoute import com.vitorpamplona.amethyst.ui.navigation.routes.isSameRoute +import com.vitorpamplona.amethyst.ui.note.PayViaIntentScreen import com.vitorpamplona.amethyst.ui.note.nip22Comments.ReplyCommentPostScreen import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountSwitcherAndLeftDrawerLayout @@ -137,6 +138,8 @@ fun AppNavigation( composableFromBottomArgs { PeopleListMetadataScreen(it.dTag, accountViewModel, nav) } composableFromBottomArgs { FollowPackMetadataScreen(it.dTag, accountViewModel, nav) } + composableFromBottomArgs { PayViaIntentScreen(it.paymentId, accountViewModel, nav) } + composableFromBottomArgs { NewUserMetadataScreen(nav, accountViewModel) } composable { SearchScreen(accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt index 778073e83..becfc33bc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt @@ -252,6 +252,11 @@ sealed class Route { val version: String? = null, val draft: String? = null, ) : Route() + + @Serializable + data class ManualZapSplitPayment( + val paymentId: String, + ) : Route() } inline fun isBaseRoute(navController: NavHostController): Boolean = navController.currentBackStackEntry?.destination?.hasRoute() == true @@ -310,6 +315,7 @@ fun getRouteWithArguments(navController: NavHostController): Route? { dest.hasRoute() -> entry.toRoute() dest.hasRoute() -> entry.toRoute() dest.hasRoute() -> entry.toRoute() + dest.hasRoute() -> entry.toRoute() else -> { null 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 22183a391..263b563c7 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,7 +52,6 @@ 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 @@ -81,6 +80,7 @@ import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.components.toasts.StringToastMsg 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.routeToMessage import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel @@ -102,14 +102,14 @@ import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists import com.vitorpamplona.quartz.nip01Core.core.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 import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import kotlin.math.roundToInt +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid @Preview @Composable @@ -499,7 +499,7 @@ private fun RenderOptionBeforeVote( } @Composable -@OptIn(ExperimentalFoundationApi::class) +@OptIn(ExperimentalFoundationApi::class, ExperimentalUuidApi::class) fun ZapVote( baseNote: Note, poolOption: PollOption, @@ -513,14 +513,8 @@ fun ZapVote( val isLoggedUser by remember { derivedStateOf { accountViewModel.isLoggedUser(baseNote.author) } } var wantsToZap by remember { mutableStateOf(false) } - var wantsToPay by remember { - mutableStateOf>( - persistentListOf(), - ) - } var zappingProgress by remember { mutableFloatStateOf(0f) } - var zapStartingTime by remember { mutableLongStateOf(0L) } var showErrorMessageDialog by remember { mutableStateOf(null) } @@ -563,7 +557,6 @@ fun ZapVote( accountViewModel.zapAmountChoices().size == 1 && pollViewModel.isValidInputVoteAmount(accountViewModel.zapAmountChoices().first()) ) { - zapStartingTime = TimeUtils.now() accountViewModel.zap( baseNote, accountViewModel.zapAmountChoices().first() * 1000, @@ -584,12 +577,13 @@ fun ZapVote( ), ) { if (wantsToZap) { + val context = LocalContext.current FilteredZapAmountChoicePopup( baseNote, accountViewModel, pollViewModel, poolOption.option, - onZapStarts = { zapStartingTime = TimeUtils.now() }, + onZapStarts = { }, onDismiss = { wantsToZap = false zappingProgress = 0f @@ -600,33 +594,17 @@ fun ZapVote( zappingProgress = 0f }, 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 - showErrorMessageDialog = - StringToastMsg( - stringRes(context, R.string.error_dialog_zap_error), - it.error, - ) - } - }, - justShowError = { - scope.launch { - showErrorMessageDialog = - StringToastMsg( - stringRes(context, R.string.error_dialog_zap_error), - it.error, - ) + onPayViaIntent = { + if (it.size == 1) { + val payable = it.first() + payViaIntent(payable.invoice, context, { }) { error -> + zappingProgress = 0f + showErrorMessageDialog = StringToastMsg(stringRes(context, R.string.error_dialog_zap_error), error) + } + } else { + val uid = Uuid.random().toString() + accountViewModel.tempManualPaymentCache.put(uid, it) + nav.nav(Route.ManualZapSplitPayment(uid)) } }, ) 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 20560510c..bd8d3d7f4 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 @@ -114,6 +114,7 @@ import com.vitorpamplona.amethyst.ui.components.AnimatedBorderTextCornerRadius import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer +import com.vitorpamplona.amethyst.ui.components.toasts.multiline.UserBasedErrorMessage import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.routes.routeReplyTo @@ -169,6 +170,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlin.math.roundToInt +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid @Composable fun ReactionsRow( @@ -970,7 +973,7 @@ private fun likeClick( } @Composable -@OptIn(ExperimentalFoundationApi::class) +@OptIn(ExperimentalFoundationApi::class, ExperimentalUuidApi::class) fun ZapReaction( baseNote: Note, grayTint: Color, @@ -983,12 +986,6 @@ fun ZapReaction( var wantsToZap by remember { mutableStateOf(false) } var wantsToChangeZapAmount by remember { mutableStateOf(false) } var wantsToSetCustomZap by remember { mutableStateOf(false) } - var wantsToPay by - remember(baseNote) { - mutableStateOf>( - persistentListOf(), - ) - } val context = LocalContext.current val scope = rememberCoroutineScope() @@ -1022,7 +1019,19 @@ fun ZapReaction( accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, message, user) } }, - onPayViaIntent = { wantsToPay = it }, + onPayViaIntent = { + if (it.size == 1) { + val payable = it.first() + payViaIntent(payable.invoice, context, { }) { error -> + zappingProgress = 0f + accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, UserBasedErrorMessage(error, payable.info.user)) + } + } else { + val uid = Uuid.random().toString() + accountViewModel.tempManualPaymentCache.put(uid, it) + nav.nav(Route.ManualZapSplitPayment(uid)) + } + }, ) } }, @@ -1053,7 +1062,19 @@ fun ZapReaction( } }, onProgress = { scope.launch(Dispatchers.Main) { zappingProgress = it } }, - onPayViaIntent = { wantsToPay = it }, + onPayViaIntent = { + if (it.size == 1) { + val payable = it.first() + payViaIntent(payable.invoice, context, { }) { error -> + zappingProgress = 0f + accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, UserBasedErrorMessage(error, payable.info.user)) + } + } else { + val uid = Uuid.random().toString() + accountViewModel.tempManualPaymentCache.put(uid, it) + nav.nav(Route.ManualZapSplitPayment(uid)) + } + }, ) } @@ -1064,26 +1085,6 @@ fun ZapReaction( ) } - 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) - } - }, - ) - } - if (wantsToSetCustomZap) { ZapCustomDialog( onZapStarts = { zapStartingTime = TimeUtils.now() }, @@ -1095,7 +1096,19 @@ fun ZapReaction( } }, onProgress = { scope.launch(Dispatchers.Main) { zappingProgress = it } }, - onPayViaIntent = { wantsToPay = it }, + onPayViaIntent = { + if (it.size == 1) { + val payable = it.first() + payViaIntent(payable.invoice, context, { }) { error -> + zappingProgress = 0f + accountViewModel.toastManager.toast(R.string.error_dialog_zap_error, UserBasedErrorMessage(error, payable.info.user)) + } + } else { + val uid = Uuid.random().toString() + accountViewModel.tempManualPaymentCache.put(uid, it) + nav.nav(Route.ManualZapSplitPayment(uid)) + } + }, accountViewModel = accountViewModel, baseNote = baseNote, ) 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 127abf1df..019455527 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 @@ -26,6 +26,7 @@ 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.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn @@ -67,10 +68,10 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.ZapPaymentHandler -import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge import com.vitorpamplona.amethyst.ui.components.TextSpinner import com.vitorpamplona.amethyst.ui.components.TitleExplainer import com.vitorpamplona.amethyst.ui.components.toasts.multiline.UserBasedErrorMessage +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -286,52 +287,42 @@ fun ZapButton( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun PayViaIntentDialog( - payingInvoices: ImmutableList, +fun PayViaIntentScreen( + paymentId: String, accountViewModel: AccountViewModel, - onClose: () -> Unit, - onError: (UserBasedErrorMessage) -> Unit, - justShowError: (UserBasedErrorMessage) -> Unit, + nav: INav, ) { - if (payingInvoices.size == 1) { - val context = LocalContext.current - val payable = payingInvoices.first() - payViaIntent(payable.invoice, context, onClose) { - onError(UserBasedErrorMessage(it, payable.info.user)) - } - } else { - Dialog( - onDismissRequest = onClose, - properties = - DialogProperties( - dismissOnClickOutside = false, - usePlatformDefaultWidth = false, - decorFitsSystemWindows = false, - ), - ) { - SetDialogToEdgeToEdge() + Scaffold( + topBar = { + TopBarWithBackButton(stringRes(id = R.string.manual_zaps), nav::popBack) + }, + ) { pad -> + val list = accountViewModel.tempManualPaymentCache.get(paymentId) - Scaffold( - topBar = { - TopBarWithBackButton(stringRes(id = R.string.manual_zaps), onClose) - }, - ) { pad -> - LazyColumn( - Modifier.padding( - 16.dp, - pad.calculateTopPadding(), - 16.dp, - pad.calculateBottomPadding(), - ), - ) { - itemsIndexed( - payingInvoices, - key = { _: Int, invoice: ZapPaymentHandler.Payable -> - invoice.invoice - }, - ) { index, payable -> - DisplayPayable(index, payable, justShowError, accountViewModel) - } + if (list == null) { + Column( + Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text(stringRes(R.string.feed_is_empty)) + } + } else { + LazyColumn( + Modifier.padding( + 16.dp, + pad.calculateTopPadding(), + 16.dp, + pad.calculateBottomPadding(), + ), + ) { + itemsIndexed( + accountViewModel.tempManualPaymentCache.get(paymentId) ?: emptyList(), + key = { _: Int, invoice: ZapPaymentHandler.Payable -> + invoice.invoice + }, + ) { index, payable -> + DisplayPayable(index, payable, accountViewModel) } } } @@ -342,7 +333,6 @@ fun PayViaIntentDialog( fun DisplayPayable( index: Int, payable: ZapPaymentHandler.Payable, - justShowError: (UserBasedErrorMessage) -> Unit, accountViewModel: AccountViewModel, ) { val paid = rememberSaveable(payable) { mutableStateOf(false) } @@ -395,7 +385,10 @@ fun DisplayPayable( PayButton(isActive = !paid.value) { payViaIntent(payable.invoice, context, { paid.value = true }) { - justShowError(UserBasedErrorMessage(it, null)) + accountViewModel.toastManager.toast( + R.string.error_dialog_zap_error, + UserBasedErrorMessage(it, payable.info.user), + ) } } } 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 66283da30..3e44b809e 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 @@ -175,6 +175,8 @@ class AccountViewModel( val toastManager = ToastManager() val feedStates = AccountFeedContentStates(account, viewModelScope) + val tempManualPaymentCache = LruCache>(5) + @OptIn(ExperimentalCoroutinesApi::class) val notificationHasNewItems = combineTransform(