diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt index 33722468d..b4ad1a4e6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt @@ -227,22 +227,6 @@ fun NewUserMetadataScreen( Spacer(modifier = Modifier.height(10.dp)) - OutlinedTextField( - label = { Text(text = stringRes(R.string.lightning_address)) }, - modifier = Modifier.fillMaxWidth(), - value = postViewModel.lnAddress.value, - onValueChange = { postViewModel.lnAddress.value = it }, - placeholder = { - Text( - text = "me@mylightningnode.com", - color = MaterialTheme.colorScheme.placeholderText, - ) - }, - singleLine = true, - ) - - Spacer(modifier = Modifier.height(10.dp)) - OutlinedTextField( label = { Text(text = stringRes(R.string.nip_05) + " (NIP-05)") }, modifier = Modifier.fillMaxWidth(), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt index c5ea8056f..80db28a2d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt @@ -57,8 +57,6 @@ class NewUserMetadataViewModel : ViewModel() { val website = mutableStateOf("") val pronouns = mutableStateOf("") val nip05 = mutableStateOf("") - val lnAddress = mutableStateOf("") - val lnURL = mutableStateOf("") val twitter = mutableStateOf("") val github = mutableStateOf("") @@ -82,8 +80,6 @@ class NewUserMetadataViewModel : ViewModel() { website.value = it.info.website ?: "" pronouns.value = it.info.pronouns ?: "" nip05.value = it.info.nip05 ?: "" - lnAddress.value = it.info.lud16 ?: "" - lnURL.value = it.info.lud06 ?: "" } twitter.value = "" @@ -121,8 +117,6 @@ class NewUserMetadataViewModel : ViewModel() { pronouns = pronouns.value, about = about.value, nip05 = nip05.value, - lnAddress = lnAddress.value, - lnURL = lnURL.value, ) val identities = @@ -146,8 +140,6 @@ class NewUserMetadataViewModel : ViewModel() { banner.value = "" website.value = "" nip05.value = "" - lnAddress.value = "" - lnURL.value = "" twitter.value = "" github.value = "" mastodon.value = "" diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/paymentTargets/PaymentTargetsViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/paymentTargets/PaymentTargetsViewModel.kt index 37c63c9ec..362df0c9c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/paymentTargets/PaymentTargetsViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/paymentTargets/PaymentTargetsViewModel.kt @@ -79,9 +79,15 @@ class PaymentTargetsViewModel : ViewModel() { fun savePaymentTargets() { if (isModified) { accountViewModel.launchSigner { - account.savePaymentTargets(_paymentTargets.value) - refresh() + savePaymentTargetsSuspend() } } } + + suspend fun savePaymentTargetsSuspend() { + if (isModified) { + account.savePaymentTargets(_paymentTargets.value) + refresh() + } + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt index ba4238425..a3827159d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt @@ -46,6 +46,7 @@ import androidx.compose.animation.togetherWith import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row @@ -148,6 +149,7 @@ fun UpdateZapAmountContent( onClose: () -> Unit, nip47uri: String? = null, accountViewModel: AccountViewModel, + trailingContent: @Composable ColumnScope.() -> Unit = {}, ) { val context = LocalContext.current val clipboardManager = LocalClipboard.current @@ -649,6 +651,8 @@ fun UpdateZapAmountContent( } } + trailingContent() + Spacer(modifier = Modifier.height(16.dp)) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountViewModel.kt index 5125b31e9..33a090373 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountViewModel.kt @@ -84,6 +84,12 @@ class UpdateZapAmountViewModel : ViewModel() { } fun sendPost() { + accountViewModel.launchSigner { + sendPostSuspend() + } + } + + suspend fun sendPostSuspend() { val nip47Update = if (walletConnectRelay.text.isNotBlank() && walletConnectPubkey.text.isNotBlank()) { val pubkeyHex = @@ -110,7 +116,7 @@ class UpdateZapAmountViewModel : ViewModel() { null } - accountViewModel.updateZapAmounts(amountSet, selectedZapType, nip47Update) + accountViewModel.account.updateZapAmounts(amountSet, selectedZapType, nip47Update) nextAmount = TextFieldValue("") } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt index fad9ebf31..c501ac432 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt @@ -29,7 +29,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.AccountBalanceWallet import androidx.compose.material.icons.outlined.Bolt import androidx.compose.material.icons.outlined.CloudUpload import androidx.compose.material.icons.outlined.DeleteForever @@ -122,13 +121,6 @@ fun AllSettingsScreen( onClick = { nav.nav(Route.EditMediaServers) }, ) HorizontalDivider() - SettingsNavigationRow( - title = R.string.payment_targets, - icon = Icons.Outlined.AccountBalanceWallet, - tint = tint, - onClick = { nav.nav(Route.EditPaymentTargets) }, - ) - HorizontalDivider() SettingsNavigationRow( title = R.string.reactions, icon = Icons.Outlined.FavoriteBorder, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt index 4b6036510..eab38a637 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt @@ -20,20 +20,45 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Delete import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.actions.paymentTargets.PaymentTargetAddField +import com.vitorpamplona.amethyst.ui.actions.paymentTargets.PaymentTargetsViewModel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountContent import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory +import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.WalletViewModel +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.SettingsCategorySpacingModifier +import com.vitorpamplona.amethyst.ui.theme.grayText +import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.experimental.nipA3.PaymentTarget @Composable fun NIP47SetupScreen( @@ -44,17 +69,27 @@ fun NIP47SetupScreen( val postViewModel: UpdateZapAmountViewModel = viewModel() postViewModel.init(accountViewModel) + val walletViewModel: WalletViewModel = viewModel() + walletViewModel.init(accountViewModel) + + val paymentTargetsViewModel: PaymentTargetsViewModel = viewModel() + paymentTargetsViewModel.init(accountViewModel) + LaunchedEffect(accountViewModel, postViewModel) { postViewModel.load() + walletViewModel.loadLnAddress() + paymentTargetsViewModel.load() } - NIP47SetupScreen(postViewModel, accountViewModel, nav, nip47) + NIP47SetupScreen(postViewModel, walletViewModel, paymentTargetsViewModel, accountViewModel, nav, nip47) } @OptIn(ExperimentalMaterial3Api::class) @Composable fun NIP47SetupScreen( postViewModel: UpdateZapAmountViewModel, + walletViewModel: WalletViewModel, + paymentTargetsViewModel: PaymentTargetsViewModel, accountViewModel: AccountViewModel, nav: INav, nip47: String?, @@ -66,20 +101,125 @@ fun NIP47SetupScreen( isActive = postViewModel::hasChanged, onCancel = { postViewModel.cancel() + paymentTargetsViewModel.refresh() nav.popBack() }, onPost = { - postViewModel.sendPost() + accountViewModel.launchSigner { + postViewModel.sendPostSuspend() + walletViewModel.saveLnAddressSuspend() + paymentTargetsViewModel.savePaymentTargetsSuspend() + } nav.popBack() }, ) }, ) { Column(Modifier.padding(it)) { - UpdateZapAmountContent(postViewModel, onClose = { - postViewModel.cancel() - nav.popBack() - }, nip47, accountViewModel) + UpdateZapAmountContent( + postViewModel, + onClose = { + postViewModel.cancel() + paymentTargetsViewModel.refresh() + nav.popBack() + }, + nip47, + accountViewModel, + ) { + HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) + LightningAddressSetupSection(walletViewModel) + HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) + PaymentTargetsInlineSection(paymentTargetsViewModel) + } + } + } +} + +@Composable +private fun LightningAddressSetupSection(walletViewModel: WalletViewModel) { + val lnAddress by walletViewModel.lnAddress.collectAsState() + + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = stringRes(R.string.lightning_address), + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.titleSmall, + modifier = SettingsCategorySpacingModifier, + ) + OutlinedTextField( + value = lnAddress, + onValueChange = { walletViewModel.updateLnAddress(it) }, + modifier = Modifier.fillMaxWidth(), + placeholder = { + Text( + text = "me@mylightningnode.com", + color = MaterialTheme.colorScheme.placeholderText, + ) + }, + singleLine = true, + ) + } +} + +@Composable +private fun PaymentTargetsInlineSection(viewModel: PaymentTargetsViewModel) { + val targets by viewModel.paymentTargets.collectAsStateWithLifecycle() + + SettingsCategory( + R.string.payment_targets, + R.string.payment_targets_section_explainer, + SettingsCategorySpacingModifier, + ) + + if (targets.isEmpty()) { + Text( + text = stringRes(id = R.string.no_payment_targets_message), + modifier = Modifier.padding(vertical = 8.dp), + color = MaterialTheme.colorScheme.grayText, + style = MaterialTheme.typography.bodyMedium, + ) + } else { + Column { + targets.forEach { target -> + PaymentTargetInlineEntry(target = target, onDelete = { viewModel.removeTarget(target) }) + } + } + } + + PaymentTargetAddField { type, authority -> + viewModel.addTarget(type, authority) + } +} + +@Composable +private fun PaymentTargetInlineEntry( + target: PaymentTarget, + onDelete: () -> Unit, +) { + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = target.type.replaceFirstChar(Char::titlecase), + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = target.authority, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.grayText, + ) + } + IconButton(onClick = onDelete) { + Icon( + imageVector = Icons.Rounded.Delete, + contentDescription = stringRes(id = R.string.delete_payment_target), + ) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletReceiveScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletReceiveScreen.kt index c0451ede7..af1a106c2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletReceiveScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletReceiveScreen.kt @@ -82,7 +82,7 @@ fun WalletReceiveScreen( val walletViewModel: WalletViewModel = viewModel() LaunchedEffect(accountViewModel) { - walletViewModel.init(accountViewModel.account) + walletViewModel.init(accountViewModel) } DisposableEffect(Unit) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt index 8ef787815..d4935ea94 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt @@ -36,6 +36,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.List import androidx.compose.material.icons.filled.ArrowDownward import androidx.compose.material.icons.filled.ArrowUpward +import androidx.compose.material.icons.outlined.Settings import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator @@ -73,7 +74,7 @@ fun WalletScreen( nav: INav, ) { val walletViewModel: WalletViewModel = viewModel() - walletViewModel.init(accountViewModel.account) + walletViewModel.init(accountViewModel) val hasWallet by walletViewModel.hasWalletSetup.collectAsState() @@ -89,6 +90,16 @@ fun WalletScreen( ) } }, + actions = { + if (hasWallet) { + IconButton(onClick = { nav.nav(Route.Nip47NWCSetup()) }) { + Icon( + imageVector = Icons.Outlined.Settings, + contentDescription = stringRes(R.string.settings), + ) + } + } + }, ) }, ) { padding -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletSendScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletSendScreen.kt index ca5bd2609..35cc4a696 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletSendScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletSendScreen.kt @@ -73,7 +73,7 @@ fun WalletSendScreen( val walletViewModel: WalletViewModel = viewModel() LaunchedEffect(accountViewModel) { - walletViewModel.init(accountViewModel.account) + walletViewModel.init(accountViewModel) } DisposableEffect(Unit) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt index 60235e15a..8d580d10d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt @@ -83,7 +83,7 @@ fun WalletTransactionsScreen( val walletViewModel: WalletViewModel = viewModel() LaunchedEffect(accountViewModel) { - walletViewModel.init(accountViewModel.account) + walletViewModel.init(accountViewModel) walletViewModel.fetchTransactions() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt index 7737923a0..ce8ceddff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBalanceMethod import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBalanceSuccessResponse import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetInfoMethod @@ -86,10 +87,14 @@ private const val PAYMENT_FAILED = "Payment failed" class WalletViewModel : ViewModel() { private var account: Account? = null + private var accountViewModel: AccountViewModel? = null private val _hasWalletSetup = MutableStateFlow(false) val hasWalletSetup = _hasWalletSetup.asStateFlow() + private val _lnAddress = MutableStateFlow("") + val lnAddress = _lnAddress.asStateFlow() + private val _balanceSats = MutableStateFlow(null) val balanceSats = _balanceSats.asStateFlow() @@ -137,15 +142,46 @@ class WalletViewModel : ViewModel() { onTimeout() } - fun init(account: Account) { - this.account = account - _hasWalletSetup.value = account.nip47SignerState.hasWalletConnectSetup() + fun init(accountViewModel: AccountViewModel) { + this.accountViewModel = accountViewModel + this.account = accountViewModel.account + _hasWalletSetup.value = accountViewModel.account.nip47SignerState.hasWalletConnectSetup() } fun refreshWalletSetup() { _hasWalletSetup.value = account?.nip47SignerState?.hasWalletConnectSetup() == true } + fun loadLnAddress() { + viewModelScope.launch(Dispatchers.IO) { + val info = + account + ?.userProfile() + ?.metadataOrNull() + ?.flow + ?.value + ?.info + _lnAddress.value = info?.lud16 ?: "" + } + } + + fun updateLnAddress(address: String) { + _lnAddress.value = address + } + + fun saveLnAddress() { + val accountVm = accountViewModel ?: return + accountVm.launchSigner { + saveLnAddressSuspend() + } + } + + suspend fun saveLnAddressSuspend() { + val acc = account ?: return + val event = acc.userMetadata.sendNewUserMetadata(lnAddress = _lnAddress.value) + acc.sendLiterallyEverywhere(event) + } + fun fetchBalance() { val acc = account ?: return viewModelScope.launch(Dispatchers.IO) {