Turn the Pay zaps via intent dialog into a full screen with route

This commit is contained in:
Vitor Pamplona
2025-11-15 16:41:47 -05:00
parent fef78cd557
commit f7e72c57d3
7 changed files with 140 additions and 140 deletions
@@ -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<ImmutableList<Long>?>(null) }
var wantsToPay by remember(baseNote) {
mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>(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))
}
},
)
@@ -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<Route.PeopleListMetadataEdit> { PeopleListMetadataScreen(it.dTag, accountViewModel, nav) }
composableFromBottomArgs<Route.FollowPackMetadataEdit> { FollowPackMetadataScreen(it.dTag, accountViewModel, nav) }
composableFromBottomArgs<Route.ManualZapSplitPayment> { PayViaIntentScreen(it.paymentId, accountViewModel, nav) }
composableFromBottomArgs<Route.EditProfile> { NewUserMetadataScreen(nav, accountViewModel) }
composable<Route.Search> { SearchScreen(accountViewModel, nav) }
@@ -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 <reified T : Route> isBaseRoute(navController: NavHostController): Boolean = navController.currentBackStackEntry?.destination?.hasRoute<T>() == true
@@ -310,6 +315,7 @@ fun getRouteWithArguments(navController: NavHostController): Route? {
dest.hasRoute<Route.PeopleListManagement>() -> entry.toRoute<Route.PeopleListManagement>()
dest.hasRoute<Route.NewGroupDM>() -> entry.toRoute<Route.NewGroupDM>()
dest.hasRoute<Route.UserSettings>() -> entry.toRoute<Route.UserSettings>()
dest.hasRoute<Route.ManualZapSplitPayment>() -> entry.toRoute<Route.ManualZapSplitPayment>()
else -> {
null
@@ -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<ImmutableList<ZapPaymentHandler.Payable>>(
persistentListOf(),
)
}
var zappingProgress by remember { mutableFloatStateOf(0f) }
var zapStartingTime by remember { mutableLongStateOf(0L) }
var showErrorMessageDialog by remember { mutableStateOf<StringToastMsg?>(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))
}
},
)
@@ -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<ImmutableList<ZapPaymentHandler.Payable>>(
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,
)
@@ -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<ZapPaymentHandler.Payable>,
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),
)
}
}
}
@@ -175,6 +175,8 @@ class AccountViewModel(
val toastManager = ToastManager()
val feedStates = AccountFeedContentStates(account, viewModelScope)
val tempManualPaymentCache = LruCache<String, List<ZapPaymentHandler.Payable>>(5)
@OptIn(ExperimentalCoroutinesApi::class)
val notificationHasNewItems =
combineTransform(